mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
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:
parent
ff9f63c5e0
commit
bf5a5b839c
|
@ -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
3
lib/semver/README.md
Normal 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)
|
858
lib/semver/include/semver/semver.hpp
Normal file
858
lib/semver/include/semver/semver.hpp
Normal 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
|
22
resources/licenses/semver.txt
Normal file
22
resources/licenses/semver.txt
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ¤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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
42
tests/src/Updates.cpp
Normal 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";
|
||||
}
|
Loading…
Reference in a new issue