Fix version checking (#4329)

https://github.com/Neargye/semver

* Use semver library for version downgrade checking

* Add test validating our current version is valid semver
This commit is contained in:
pajlada 2023-01-26 19:22:48 +01:00 committed by GitHub
parent ff9f63c5e0
commit bf5a5b839c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 981 additions and 33 deletions

View file

@ -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)

3
lib/semver/README.md Normal file
View file

@ -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)

View file

@ -0,0 +1,858 @@
// _____ _ _
// / ____| | | (_)
// | (___ ___ _ __ ___ __ _ _ __ | |_ _ ___
// \___ \ / _ \ '_ ` _ \ / _` | '_ \| __| |/ __|
// ____) | __/ | | | | | (_| | | | | |_| | (__
// |_____/ \___|_| |_| |_|\__,_|_| |_|\__|_|\___|
// __ __ _ _ _____
// \ \ / / (_) (_) / ____|_ _
// \ \ / /__ _ __ ___ _ ___ _ __ _ _ __ __ _ | | _| |_ _| |_
// \ \/ / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | | | |_ _|_ _|
// \ / __/ | \__ \ | (_) | | | | | | | | (_| | | |____|_| |_|
// \/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | \_____|
// https://github.com/Neargye/semver __/ |
// version 0.3.0 |___/
//
// Licensed under the MIT License <http://opensource.org/licenses/MIT>.
// SPDX-License-Identifier: MIT
// Copyright (c) 2018 - 2021 Daniil Goncharov <neargye@gmail.com>.
// Copyright (c) 2020 - 2021 Alexander Gorbunov <naratzul@gmail.com>.
//
// 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 <cstddef>
#include <cstdint>
#include <iosfwd>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#if __has_include(<charconv>)
#include <charconv>
#else
#include <system_error>
#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 <stdexcept>
# define SEMVER_THROW(msg) (throw std::invalid_argument{msg})
#else
# include <cassert>
# include <cstdlib>
# 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(<charconv>)
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(<major>) + 1(.) + 3(<minor>) + 1(.) + 3(<patch>) + 1(-) + 5(<prerelease>) + 1(.) + 3(<prereleaseversion>) = 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(<major>) + 1(.) + 1(<minor>) + 1(.) + 1(<patch>) = 5.
inline constexpr auto min_version_string_length = 5;
constexpr char to_lower(char c) noexcept {
return (c >= 'A' && c <= 'Z') ? static_cast<char>(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<std::uint8_t>(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<std::uint8_t>(alpha.length());
} else if (t == prerelease::beta) {
return static_cast<std::uint8_t>(beta.length());
} else if (t == prerelease::rc) {
return static_cast<std::uint8_t>(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<char>('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<std::uint8_t>::max)()) {
d = static_cast<std::uint8_t>(t);
return first;
}
}
return nullptr;
}
constexpr const char* from_chars(const char* first, const char* last, std::optional<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<std::uint8_t>::max)()) {
d = static_cast<std::uint8_t>(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 <typename T, typename = void>
struct resize_uninitialized {
static auto resize(T& str, std::size_t size) -> std::void_t<decltype(str.resize(size))> {
str.resize(size);
}
};
template <typename T>
struct resize_uninitialized<T, std::void_t<decltype(std::declval<T>().__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<std::uint8_t> prerelease_number = std::nullopt;
constexpr version(std::uint8_t mj,
std::uint8_t mn,
std::uint8_t pt,
prerelease prt = prerelease::none,
std::optional<std::uint8_t> 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<std::string>::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 {
// (<major>) + 1(.) + (<minor>) + 1(.) + (<patch>)
auto length = detail::length(major) + detail::length(minor) + detail::length(patch) + 2;
if (prerelease_type != prerelease::none) {
// + 1(-) + (<prerelease>)
length += detail::length(prerelease_type) + 1;
if (prerelease_number.has_value()) {
// + 1(.) + (<prereleaseversion>)
length += detail::length(prerelease_number.value()) + 1;
}
}
return static_cast<std::uint8_t>(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<std::uint8_t>(prerelease_type) - static_cast<std::uint8_t>(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<version> 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 <typename Char, typename Traits>
inline std::basic_ostream<Char, Traits>& operator<<(std::basic_ostream<Char, Traits>& 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<std::uint8_t> 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

View file

@ -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.

View file

@ -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

View file

@ -3,6 +3,27 @@
#include <QString>
#include <QtGlobal>
/**
* 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)

View file

@ -15,6 +15,7 @@
#include <QMessageBox>
#include <QProcess>
#include <QRegularExpression>
#include <semver/semver.hpp>
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 &current)
{
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 &current)
{
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
{

View file

@ -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 &current);
void checkForUpdates();
const QString &getCurrentVersion() const;
const QString &getOnlineVersion() const;

View file

@ -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

View file

@ -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!
)

42
tests/src/Updates.cpp Normal file
View file

@ -0,0 +1,42 @@
#include "singletons/Updates.hpp"
#include "common/Version.hpp"
#include <gtest/gtest.h>
#include <semver/semver.hpp>
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";
}