From ee639fa1df25bd607d23b35d58522e7e523f9d46 Mon Sep 17 00:00:00 2001 From: Gurkirat Singh Date: Thu, 21 Dec 2023 15:49:11 +0530 Subject: [PATCH] Libraries: Implement SemVer for version parsing and comparisons Semantic Versioning (SemVer) is a versioning scheme for software that uses MAJOR.MINOR.PATCH format. MAJOR for significant, possibly breaking changes; MINOR for backward-compatible additions; PATCH for bug fixes. It aids communication, compatibility prediction, and dependency management. In apps dependent on specific library versions, SemVer guides parsing and validates compatibility, ensuring apps use appropriate dependencies. ::= | "-" | "+" | "-" "+" --- Tests/CMakeLists.txt | 1 + Tests/LibSemVer/CMakeLists.txt | 8 + Tests/LibSemVer/TestFromStringView.cpp | 104 +++++++ Tests/LibSemVer/TestSemVer.cpp | 248 ++++++++++++++++ Userland/Libraries/CMakeLists.txt | 1 + Userland/Libraries/LibSemVer/CMakeLists.txt | 5 + Userland/Libraries/LibSemVer/SemVer.cpp | 303 ++++++++++++++++++++ Userland/Libraries/LibSemVer/SemVer.h | 100 +++++++ 8 files changed, 770 insertions(+) create mode 100644 Tests/LibSemVer/CMakeLists.txt create mode 100644 Tests/LibSemVer/TestFromStringView.cpp create mode 100644 Tests/LibSemVer/TestSemVer.cpp create mode 100644 Userland/Libraries/LibSemVer/CMakeLists.txt create mode 100644 Userland/Libraries/LibSemVer/SemVer.cpp create mode 100644 Userland/Libraries/LibSemVer/SemVer.h diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 9dfe755bbf..eff2a1c661 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(LibLocale) add_subdirectory(LibMarkdown) add_subdirectory(LibPDF) add_subdirectory(LibRegex) +add_subdirectory(LibSemVer) add_subdirectory(LibSQL) add_subdirectory(LibTest) add_subdirectory(LibTextCodec) diff --git a/Tests/LibSemVer/CMakeLists.txt b/Tests/LibSemVer/CMakeLists.txt new file mode 100644 index 0000000000..de6b241f41 --- /dev/null +++ b/Tests/LibSemVer/CMakeLists.txt @@ -0,0 +1,8 @@ +set(TEST_SOURCES + TestFromStringView.cpp + TestSemVer.cpp +) + +foreach(source IN LISTS TEST_SOURCES) + serenity_test("${source}" LibSemVer LIBS LibSemVer) +endforeach() diff --git a/Tests/LibSemVer/TestFromStringView.cpp b/Tests/LibSemVer/TestFromStringView.cpp new file mode 100644 index 0000000000..59a5e2f673 --- /dev/null +++ b/Tests/LibSemVer/TestFromStringView.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, Gurkirat Singh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +TEST_CASE(parsing) // NOLINT(readability-function-cognitive-complexity) +{ + EXPECT(!SemVer::is_valid("1"sv)); + EXPECT(!SemVer::is_valid("1.2"sv)); + EXPECT(!SemVer::is_valid("1.1.2+.123"sv)); + EXPECT(!SemVer::is_valid("1.2.3-0123"sv)); + EXPECT(!SemVer::is_valid("1.2.3-0123.0123"sv)); + EXPECT(!SemVer::is_valid("+invalid"sv)); + EXPECT(!SemVer::is_valid("-invalid"sv)); + EXPECT(!SemVer::is_valid("-invalid+invalid"sv)); + EXPECT(!SemVer::is_valid("-invalid.01"sv)); + EXPECT(!SemVer::is_valid("1 .2.3-this.is.invalid"sv)); + EXPECT(!SemVer::is_valid("1.2.3-this .is. also .invalid"sv)); + EXPECT(!SemVer::is_valid("1.2.3"sv, ' ')); + EXPECT(!SemVer::is_valid("alpha"sv)); + EXPECT(!SemVer::is_valid("alpha.beta"sv)); + EXPECT(!SemVer::is_valid("alpha.beta.1"sv)); + EXPECT(!SemVer::is_valid("alpha.1"sv)); + EXPECT(!SemVer::is_valid("alpha+beta"sv)); + EXPECT(!SemVer::is_valid("alpha_beta"sv)); + EXPECT(!SemVer::is_valid("alpha."sv)); + EXPECT(!SemVer::is_valid("alpha.."sv)); + EXPECT(!SemVer::is_valid("beta"sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha_beta"sv)); + EXPECT(!SemVer::is_valid("-alpha."sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha.."sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha..1"sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha...1"sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha....1"sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha.....1"sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha......1"sv)); + EXPECT(!SemVer::is_valid("1.0.0-alpha.......1"sv)); + EXPECT(!SemVer::is_valid("01.1.1"sv)); + EXPECT(!SemVer::is_valid("1.01.1"sv)); + EXPECT(!SemVer::is_valid("1.1.01"sv)); + EXPECT(!SemVer::is_valid("1.2"sv)); + EXPECT(!SemVer::is_valid("1.2.3.DEV"sv)); + EXPECT(!SemVer::is_valid("1.2-SNAPSHOT"sv)); + EXPECT(!SemVer::is_valid("1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788"sv)); + EXPECT(!SemVer::is_valid("1.2-RC-SNAPSHOT"sv)); + EXPECT(!SemVer::is_valid("-1.0.3-gamma+b7718"sv)); + EXPECT(!SemVer::is_valid("+justmeta"sv)); + EXPECT(!SemVer::is_valid("9.8.7+meta+meta"sv)); + EXPECT(!SemVer::is_valid("9.8.7-whatever+meta+meta"sv)); + // Because of size_t overflow, it won't work work version such as 99999999999999999999999 + EXPECT(!SemVer::is_valid("99999999999999999999999.999999999999999999.99999999999999999"sv)); + EXPECT(SemVer::is_valid("1.0.4"sv)); + EXPECT(SemVer::is_valid("1.2.3"sv)); + EXPECT(SemVer::is_valid("10.20.30"sv)); + EXPECT(SemVer::is_valid("1.1.2-prerelease+meta"sv)); + EXPECT(SemVer::is_valid("1.1.2+meta"sv)); + EXPECT(SemVer::is_valid("1.1.2+meta-valid"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha"sv)); + EXPECT(SemVer::is_valid("1.0.0-beta"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha.beta"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha.beta.1"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha.1"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha0.valid"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha.0valid"sv)); + EXPECT(SemVer::is_valid("1.0.0-rc.1+build.1"sv)); + EXPECT(SemVer::is_valid("2.0.0-rc.1+build.123"sv)); + EXPECT(SemVer::is_valid("1.2.3-beta"sv)); + EXPECT(SemVer::is_valid("10.2.3-DEV-SNAPSHOT"sv)); + EXPECT(SemVer::is_valid("1.2.3-SNAPSHOT-123"sv)); + EXPECT(SemVer::is_valid("1.0.0"sv)); + EXPECT(SemVer::is_valid("2.0.0"sv)); + EXPECT(SemVer::is_valid("1.1.7"sv)); + EXPECT(SemVer::is_valid("2.0.0+build.1848"sv)); + EXPECT(SemVer::is_valid("2.0.1-alpha.1227"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha+beta"sv)); + EXPECT(SemVer::is_valid("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay"sv)); + EXPECT(SemVer::is_valid("1.2.3----RC-SNAPSHOT.12.9.1--.12+788"sv)); + EXPECT(SemVer::is_valid("1.2.3----R-S.12.9.1--.12+meta"sv)); + EXPECT(SemVer::is_valid("1.2.3----RC-SNAPSHOT.12.9.1--.12"sv)); + EXPECT(SemVer::is_valid("1.0.0+0.build.1-rc.10000aaa-kk-0.1"sv)); + EXPECT(SemVer::is_valid("1.0.0-0A.is.legal"sv)); +} + +TEST_CASE(parse_with_different_mmp_sep) +{ + // insufficient separators + EXPECT(!SemVer::is_valid("1.2-3"sv)); + EXPECT(!SemVer::is_valid("1.2-3"sv, '-')); + + // conflicting separators + EXPECT(!SemVer::is_valid("11213"sv, '1')); + + // sufficient separators + EXPECT(SemVer::is_valid("1.2.3"sv, '.')); + EXPECT(SemVer::is_valid("1-2-3"sv, '-')); + EXPECT(SemVer::is_valid("1-3-3-pre+build"sv, '-')); +} diff --git a/Tests/LibSemVer/TestSemVer.cpp b/Tests/LibSemVer/TestSemVer.cpp new file mode 100644 index 0000000000..b914693e72 --- /dev/null +++ b/Tests/LibSemVer/TestSemVer.cpp @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2023, Gurkirat Singh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +#define GET_SEMVER(expression) \ + ({ \ + auto r = (SemVer::from_string_view(expression)); \ + EXPECT(!r.is_error()); \ + r.value(); \ + }) + +#define GET_STRING(expression) \ + ({ \ + auto r = (String::from_utf8(expression)); \ + EXPECT(!r.is_error()); \ + r.value(); \ + }) + +#define IS_SAME_SCENARIO(x, y, op) \ + GET_SEMVER(x).is_same(GET_SEMVER(y), op) + +#define IS_GREATER_THAN_SCENARIO(x, y) \ + GET_SEMVER(x).is_greater_than(GET_SEMVER(y)) + +#define IS_LESSER_THAN_SCENARIO(x, y) \ + GET_SEMVER(x).is_lesser_than(GET_SEMVER(y)) + +TEST_CASE(to_string) // NOLINT(readability-function-cognitive-complexity, readability-function-size) +{ + EXPECT_EQ(GET_SEMVER("1.2.3"sv).to_string(), GET_STRING("1.2.3"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3"sv).to_string(), GET_STRING("1.2.3"sv)); + EXPECT_EQ(GET_SEMVER("10.20.30"sv).to_string(), GET_STRING("10.20.30"sv)); + EXPECT_EQ(GET_SEMVER("1.1.2-prerelease+meta"sv).to_string(), GET_STRING("1.1.2-prerelease+meta"sv)); + EXPECT_EQ(GET_SEMVER("1.1.2+meta"sv).to_string(), GET_STRING("1.1.2+meta"sv)); + EXPECT_EQ(GET_SEMVER("1.1.2+meta-valid"sv).to_string(), GET_STRING("1.1.2+meta-valid"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha"sv).to_string(), GET_STRING("1.0.0-alpha"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-beta"sv).to_string(), GET_STRING("1.0.0-beta"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha.beta"sv).to_string(), GET_STRING("1.0.0-alpha.beta"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha.beta.1"sv).to_string(), GET_STRING("1.0.0-alpha.beta.1"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha.1"sv).to_string(), GET_STRING("1.0.0-alpha.1"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha0.valid"sv).to_string(), GET_STRING("1.0.0-alpha0.valid"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha.0valid"sv).to_string(), GET_STRING("1.0.0-alpha.0valid"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-rc.1+build.1"sv).to_string(), GET_STRING("1.0.0-rc.1+build.1"sv)); + EXPECT_EQ(GET_SEMVER("2.0.0-rc.1+build.123"sv).to_string(), GET_STRING("2.0.0-rc.1+build.123"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3-beta"sv).to_string(), GET_STRING("1.2.3-beta"sv)); + EXPECT_EQ(GET_SEMVER("10.2.3-DEV-SNAPSHOT"sv).to_string(), GET_STRING("10.2.3-DEV-SNAPSHOT"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3-SNAPSHOT-123"sv).to_string(), GET_STRING("1.2.3-SNAPSHOT-123"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0"sv).to_string(), GET_STRING("1.0.0"sv)); + EXPECT_EQ(GET_SEMVER("2.0.0"sv).to_string(), GET_STRING("2.0.0"sv)); + EXPECT_EQ(GET_SEMVER("1.1.7"sv).to_string(), GET_STRING("1.1.7"sv)); + EXPECT_EQ(GET_SEMVER("2.0.0+build.1848"sv).to_string(), GET_STRING("2.0.0+build.1848"sv)); + EXPECT_EQ(GET_SEMVER("2.0.1-alpha.1227"sv).to_string(), GET_STRING("2.0.1-alpha.1227"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha+beta"sv).to_string(), GET_STRING("1.0.0-alpha+beta"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay"sv).to_string(), GET_STRING("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3----RC-SNAPSHOT.12.9.1--.12+788"sv).to_string(), GET_STRING("1.2.3----RC-SNAPSHOT.12.9.1--.12+788"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3----RC-SNAPSHOT.12.9.1--"sv).to_string(), GET_STRING("1.2.3----RC-SNAPSHOT.12.9.1--"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3----R-S.12.9.1--.12+meta"sv).to_string(), GET_STRING("1.2.3----R-S.12.9.1--.12+meta"sv)); + EXPECT_EQ(GET_SEMVER("1.2.3----RC-SNAPSHOT.12.9.1--.12"sv).to_string(), GET_STRING("1.2.3----RC-SNAPSHOT.12.9.1--.12"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0+0.build.1-rc.10000aaa-kk-0.1"sv).to_string(), GET_STRING("1.0.0+0.build.1-rc.10000aaa-kk-0.1"sv)); + EXPECT_EQ(GET_SEMVER("1.0.0-0A.is.legal"sv).to_string(), GET_STRING("1.0.0-0A.is.legal"sv)); +} + +TEST_CASE(normal_bump) // NOLINT(readability-function-cognitive-complexity) +{ + auto version = GET_SEMVER("1.1.2-prerelease+meta"sv); + + // normal bumps + auto major_bump = version.bump(SemVer::BumpType::Major); + EXPECT_EQ(major_bump.major(), version.major() + 1); + EXPECT_EQ(major_bump.minor(), 0ul); + EXPECT_EQ(major_bump.patch(), 0ul); + EXPECT(major_bump.suffix().is_empty()); + + auto minor_bump = version.bump(SemVer::BumpType::Minor); + EXPECT_EQ(minor_bump.major(), version.major()); + EXPECT_EQ(minor_bump.minor(), version.minor() + 1); + EXPECT_EQ(minor_bump.patch(), 0ul); + EXPECT(minor_bump.suffix().is_empty()); + + auto patch_bump = version.bump(SemVer::BumpType::Patch); + EXPECT_EQ(patch_bump.major(), version.major()); + EXPECT_EQ(patch_bump.minor(), version.minor()); + EXPECT_EQ(patch_bump.patch(), version.patch() + 1); + EXPECT(minor_bump.suffix().is_empty()); +} + +TEST_CASE(prerelease_bump_increment_numeric) +{ + auto version = GET_SEMVER("1.1.2-0"sv); + + auto prerelease_bump = version.bump(SemVer::BumpType::Prerelease); + EXPECT_EQ(prerelease_bump.major(), version.major()); + EXPECT_EQ(prerelease_bump.minor(), version.minor()); + EXPECT_EQ(prerelease_bump.patch(), version.patch()); + EXPECT_NE(prerelease_bump.prerelease(), version.prerelease()); + EXPECT(prerelease_bump.build_metadata().is_empty()); + + auto version_prerelease_parts = version.prerelease_identifiers(); + auto bumped_prerelease_parts = prerelease_bump.prerelease_identifiers(); + EXPECT_EQ(bumped_prerelease_parts.size(), version_prerelease_parts.size()); + EXPECT_EQ(bumped_prerelease_parts[0], "1"_string); +} + +TEST_CASE(prerelease_bump_rightmost_numeric_part) +{ + auto version = GET_SEMVER("1.1.2-a.1.0.c"sv); + + auto prerelease_bump = version.bump(SemVer::BumpType::Prerelease); + EXPECT_EQ(prerelease_bump.major(), version.major()); + EXPECT_EQ(prerelease_bump.minor(), version.minor()); + EXPECT_EQ(prerelease_bump.patch(), version.patch()); + EXPECT_NE(prerelease_bump.prerelease(), version.prerelease()); + EXPECT(prerelease_bump.build_metadata().is_empty()); + + auto version_prerelease_parts = version.prerelease_identifiers(); + auto bumped_prerelease_parts = prerelease_bump.prerelease_identifiers(); + EXPECT_EQ(bumped_prerelease_parts.size(), version_prerelease_parts.size()); + EXPECT_EQ(bumped_prerelease_parts[2], "1"_string); +} + +TEST_CASE(prerelease_bump_add_zero_if_no_numeric) +{ + auto version = GET_SEMVER("1.1.2-only.strings"sv); + + auto prerelease_bump = version.bump(SemVer::BumpType::Prerelease); + EXPECT_EQ(prerelease_bump.major(), version.major()); + EXPECT_EQ(prerelease_bump.minor(), version.minor()); + EXPECT_EQ(prerelease_bump.patch(), version.patch()); + EXPECT_NE(prerelease_bump.prerelease(), version.prerelease()); + EXPECT(prerelease_bump.build_metadata().is_empty()); + + auto version_prerelease_parts = version.prerelease_identifiers(); + auto bumped_prerelease_parts = prerelease_bump.prerelease_identifiers(); + EXPECT(bumped_prerelease_parts.size() > version_prerelease_parts.size()); + EXPECT_EQ(bumped_prerelease_parts[2], "0"_string); +} + +TEST_CASE(is_same) // NOLINT(readability-function-cognitive-complexity) +{ + // exact match + EXPECT(IS_SAME_SCENARIO("1.1.2-prerelease+meta"sv, "1.1.2-prerelease+meta"sv, SemVer::CompareType::Exact)); + EXPECT(!IS_SAME_SCENARIO("1.1.2-prerelease+meta"sv, "1.1.3-prerelease+meta"sv, SemVer::CompareType::Exact)); + EXPECT(!IS_SAME_SCENARIO("1.1.2-prerelease+meta"sv, "1.2.2-prerelease+meta"sv, SemVer::CompareType::Exact)); + EXPECT(!IS_SAME_SCENARIO("1.1.2-prerelease+meta"sv, "2.1.2-prerelease+meta"sv, SemVer::CompareType::Exact)); + EXPECT(!IS_SAME_SCENARIO("1.1.2-prerelease+meta"sv, "1.1.3-someother"sv, SemVer::CompareType::Exact)); + // major part match + EXPECT(IS_SAME_SCENARIO("1.1.2"sv, "1.1.2"sv, SemVer::CompareType::Major)); + EXPECT(IS_SAME_SCENARIO("1.1.2"sv, "1.2.2"sv, SemVer::CompareType::Major)); + EXPECT(IS_SAME_SCENARIO("1.1.2"sv, "1.1.3"sv, SemVer::CompareType::Major)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "2.1.2"sv, SemVer::CompareType::Major)); + // minor part match + EXPECT(IS_SAME_SCENARIO("1.1.2"sv, "1.1.2"sv, SemVer::CompareType::Minor)); + EXPECT(IS_SAME_SCENARIO("1.1.2"sv, "1.1.3"sv, SemVer::CompareType::Minor)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "1.2.2"sv, SemVer::CompareType::Minor)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "2.1.2"sv, SemVer::CompareType::Minor)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "2.2.2"sv, SemVer::CompareType::Minor)); + // patch part match + EXPECT(IS_SAME_SCENARIO("1.1.2"sv, "1.1.2"sv, SemVer::CompareType::Patch)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "1.1.3"sv, SemVer::CompareType::Patch)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "1.2.2"sv, SemVer::CompareType::Patch)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "2.1.2"sv, SemVer::CompareType::Patch)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "1.2.2"sv, SemVer::CompareType::Patch)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "2.1.2"sv, SemVer::CompareType::Patch)); + EXPECT(!IS_SAME_SCENARIO("1.1.2"sv, "2.2.2"sv, SemVer::CompareType::Patch)); +} + +TEST_CASE(is_greater_than) // NOLINT(readability-function-cognitive-complexity) +{ + // Just normal versions + EXPECT(IS_GREATER_THAN_SCENARIO("1.1.3"sv, "1.1.2"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("1.2.2"sv, "1.1.2"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("2.1.2"sv, "1.1.2"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("2.1.3"sv, "1.1.2"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("1.2.3"sv, "1.1.2"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("1.2.2"sv, "1.1.2"sv)); + EXPECT(!IS_GREATER_THAN_SCENARIO("1.1.2"sv, "1.1.2"sv)); + + // Basic, imbalanced prereleased testing + EXPECT(!IS_GREATER_THAN_SCENARIO("1.0.0-alpha"sv, "1.0.0-alpha"sv)); + EXPECT(!IS_GREATER_THAN_SCENARIO("1.0.0-alpha"sv, "1.0.0"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0"sv, "1.0.0-0"sv)); + + // Both versions have more than one identifiers + // 1. All numeric + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0-0.1.2"sv, "1.0.0-0.1.1"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0-0.2.0"sv, "1.0.0-0.1.2"sv)); + EXPECT(!IS_GREATER_THAN_SCENARIO("1.0.0-0.1.2"sv, "1.0.0-0.1.2"sv)); + + // 2. For non-numeric, lexical compare + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0-beta"sv, "1.0.0-alpha"sv)); + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0-0.beta"sv, "1.0.0-0.alpha"sv)); + + // 3. Either one is numeric, but not both, then numeric given low precendence + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0-0.alpha"sv, "1.0.0-0.0"sv)); + EXPECT(!IS_GREATER_THAN_SCENARIO("1.0.0-0.0"sv, "1.0.0-0.alpha"sv)); + + // 4. Prefix identifiers are same, larger has high precedence + EXPECT(IS_GREATER_THAN_SCENARIO("1.0.0-alpha.beta.gamma"sv, "1.0.0-alpha"sv)); +} + +TEST_CASE(is_lesser_than) // NOLINT(readability-function-cognitive-complexity) +{ + // This function depends on is_greater_than, so basic testing is OK + EXPECT(IS_LESSER_THAN_SCENARIO("1.1.2"sv, "1.1.3"sv)); + EXPECT(IS_LESSER_THAN_SCENARIO("1.1.2"sv, "1.2.2"sv)); + EXPECT(IS_LESSER_THAN_SCENARIO("1.1.2"sv, "2.1.2"sv)); + EXPECT(IS_LESSER_THAN_SCENARIO("1.1.2"sv, "2.1.3"sv)); + EXPECT(IS_LESSER_THAN_SCENARIO("1.1.2"sv, "1.2.3"sv)); + EXPECT(IS_LESSER_THAN_SCENARIO("1.1.2"sv, "1.2.2"sv)); + EXPECT(!IS_LESSER_THAN_SCENARIO("1.1.2"sv, "1.1.2"sv)); +} + +TEST_CASE(satisfies) // NOLINT(readability-function-cognitive-complexity) +{ + auto version = GET_SEMVER("1.1.2-prerelease+meta"sv); + + EXPECT(version.satisfies("1.1.2-prerelease+meta"sv)); + EXPECT(!version.satisfies("1.2.2-prerelease+meta"sv)); + EXPECT(!version.satisfies("!=1.1.2-prerelease+meta"sv)); + EXPECT(version.satisfies("!=1.2.2-prerelease+meta"sv)); + EXPECT(version.satisfies("=1.1.2"sv)); + EXPECT(version.satisfies("=1.1.2-prerelease+meta"sv)); + EXPECT(!version.satisfies("=1.1.3"sv)); + EXPECT(!version.satisfies("==1.1.3-prerelease+meta"sv)); + EXPECT(version.satisfies("==1.1.2-prerelease"sv)); + EXPECT(version.satisfies("==1.1.2-prerelease+meta"sv)); + EXPECT(!version.satisfies("<1.1.1-prerelease+meta"sv)); + EXPECT(!version.satisfies("<1.1.2-prerelease+meta"sv)); + EXPECT(version.satisfies("<1.1.3-prerelease+meta"sv)); + EXPECT(version.satisfies(">1.1.1-prerelease+meta"sv)); + EXPECT(!version.satisfies(">1.1.2-prerelease+meta"sv)); + EXPECT(!version.satisfies(">1.1.3-prerelease+meta"sv)); + EXPECT(version.satisfies(">=1.1.1-prerelease+meta"sv)); + EXPECT(version.satisfies(">=1.1.2-prerelease+meta"sv)); + EXPECT(!version.satisfies(">=1.1.3-prerelease+meta"sv)); + EXPECT(!version.satisfies("<=1.1.1-prerelease+meta"sv)); + EXPECT(version.satisfies("<=1.1.2-prerelease+meta"sv)); + EXPECT(version.satisfies("<=1.1.3-prerelease+meta"sv)); + EXPECT(!version.satisfies("HELLO1.1.2-prerelease+meta"sv)); +} diff --git a/Userland/Libraries/CMakeLists.txt b/Userland/Libraries/CMakeLists.txt index a058b8d62f..a78830232d 100644 --- a/Userland/Libraries/CMakeLists.txt +++ b/Userland/Libraries/CMakeLists.txt @@ -49,6 +49,7 @@ add_subdirectory(LibProtocol) add_subdirectory(LibRegex) add_subdirectory(LibRIFF) add_subdirectory(LibSanitizer) +add_subdirectory(LibSemVer) add_subdirectory(LibSoftGPU) add_subdirectory(LibSQL) add_subdirectory(LibSymbolication) diff --git a/Userland/Libraries/LibSemVer/CMakeLists.txt b/Userland/Libraries/LibSemVer/CMakeLists.txt new file mode 100644 index 0000000000..6e489f1239 --- /dev/null +++ b/Userland/Libraries/LibSemVer/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SOURCES + SemVer.cpp +) + +serenity_lib(LibSemVer semver) diff --git a/Userland/Libraries/LibSemVer/SemVer.cpp b/Userland/Libraries/LibSemVer/SemVer.cpp new file mode 100644 index 0000000000..e0db62e70e --- /dev/null +++ b/Userland/Libraries/LibSemVer/SemVer.cpp @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2023, Gurkirat Singh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace SemVer { +String SemVer::suffix() const +{ + StringBuilder sb; + if (!m_prerelease_identifiers.is_empty()) + sb.appendff("-{}", prerelease()); + if (!m_build_metadata_identifiers.is_empty()) + sb.appendff("+{}", build_metadata()); + return sb.to_string().release_value_but_fixme_should_propagate_errors(); +} + +String SemVer::to_string() const +{ + return String::formatted("{}{}{}{}{}{}", m_major, m_number_separator, m_minor, m_number_separator, m_patch, suffix()).release_value_but_fixme_should_propagate_errors(); +} + +SemVer SemVer::bump(BumpType type) const +{ + switch (type) { + case BumpType::Major: + return SemVer(m_major + 1, 0, 0, m_number_separator); + case BumpType::Minor: + return SemVer(m_major, m_minor + 1, 0, m_number_separator); + case BumpType::Patch: + return SemVer(m_major, m_minor, m_patch + 1, m_number_separator); + case BumpType::Prerelease: { + Vector prerelease_identifiers = m_prerelease_identifiers; + bool is_found = false; + + // Unlike comparision, prerelease bumps take from RTL. + for (auto& identifier : AK::ReverseWrapper::in_reverse(prerelease_identifiers)) { + auto numeric_identifier = identifier.to_number(); + if (numeric_identifier.has_value()) { + is_found = true; + identifier = String::formatted("{}", numeric_identifier.value() + 1).release_value_but_fixme_should_propagate_errors(); + break; + } + } + + // Append 0 identifier if there is no numeric found to be bumped. + if (!is_found) + prerelease_identifiers.append("0"_string); + + return SemVer(m_major, m_minor, m_patch, m_number_separator, prerelease_identifiers, {}); + } + default: + VERIFY_NOT_REACHED(); + } +} + +bool SemVer::is_same(SemVer const& other, CompareType compare_type) const +{ + switch (compare_type) { + case CompareType::Major: + return m_major == other.m_major; + case CompareType::Minor: + return m_major == other.m_major && m_minor == other.m_minor; + case CompareType::Patch: + return m_major == other.m_major && m_minor == other.m_minor && m_patch == other.m_patch; + default: + // Build metadata MUST be ignored when determining version precedence. + return m_major == other.m_major && m_minor == other.m_minor && m_patch == other.m_patch && prerelease() == other.prerelease(); + } +} + +bool SemVer::is_greater_than(SemVer const& other) const +{ + // Priortize the normal version string. + // Precedence is determined by the first difference when comparing them from left to right. + // Major > Minor > Patch + if (m_major > other.m_major || m_minor > other.m_minor || m_patch > other.m_patch) + return true; + + // When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. + // Example: 1.0.0-alpha < 1.0.0 + if (prerelease() == other.prerelease() || other.prerelease().is_empty()) + return false; + if (prerelease().is_empty()) + return true; + + // Both the versions have non-zero length of pre-release identifiers. + for (size_t i = 0; i < min(prerelease_identifiers().size(), other.prerelease_identifiers().size()); ++i) { + auto const this_numerical_identifier = m_prerelease_identifiers[i].to_number(); + auto const other_numerical_identifier = other.m_prerelease_identifiers[i].to_number(); + + // 1. Identifiers consisting of only digits are compared numerically. + if (this_numerical_identifier.has_value() && other_numerical_identifier.has_value()) { + auto const this_value = this_numerical_identifier.value(); + auto const other_value = other_numerical_identifier.value(); + + if (this_value == other_value) { + continue; + } + return this_value > other_value; + } + + // 2. Identifiers with letters or hyphens are compared lexically in ASCII sort order. + if (!this_numerical_identifier.has_value() && !other_numerical_identifier.has_value()) { + if (m_prerelease_identifiers[i] == other.m_prerelease_identifiers[i]) { + continue; + } + return m_prerelease_identifiers[i] > other.m_prerelease_identifiers[i]; + } + + // 3. Numeric identifiers always have lower precedence than non-numeric identifiers. + if (this_numerical_identifier.has_value() && !other_numerical_identifier.has_value()) + return false; + if (!this_numerical_identifier.has_value() && other_numerical_identifier.has_value()) + return true; + } + + // 4. If all of the preceding identifiers are equal, larger set of pre-release fields has a higher precedence than a smaller set. + return m_prerelease_identifiers.size() > other.m_prerelease_identifiers.size(); +} + +bool SemVer::satisfies(StringView const& semver_spec) const +{ + GenericLexer lexer(semver_spec.trim_whitespace()); + if (lexer.tell_remaining() == 0) + return false; + + auto compare_op = lexer.consume_until([](auto const& ch) { return ch >= '0' && ch <= '9'; }); + + auto spec_version = MUST(from_string_view(lexer.consume_all())); + // Lenient compare, tolerance for any patch and pre-release. + if (compare_op.is_empty()) + return is_same(spec_version, CompareType::Minor); + if (compare_op == "!="sv) + return !is_same(spec_version); + + // Adds strictness based on number of equal sign. + if (compare_op == "="sv) + return is_same(spec_version, CompareType::Patch); + // Exact version string match. + if (compare_op == "=="sv) + return is_same(spec_version); + + // Current version is greater than spec. + if (compare_op == ">"sv) + return is_greater_than(spec_version); + if (compare_op == "<"sv) + return is_lesser_than(spec_version); + if (compare_op == ">="sv) + return is_same(spec_version) || is_greater_than(spec_version); + if (compare_op == "<="sv) + return is_same(spec_version) || !is_greater_than(spec_version); + + return false; +} + +ErrorOr from_string_view(StringView const& version, char normal_version_separator) +{ + if (is_ascii_space(normal_version_separator) || is_ascii_digit(normal_version_separator)) { + return Error::from_string_view("Version separator can't be a space or digit character"sv); + } + + if (version.count(normal_version_separator) < 2) + return Error::from_string_view("Insufficient occurrences of version separator"sv); + + if (version.count('+') > 1) + return Error::from_string_view("Build metadata must be defined at most once"sv); + + // Checks for the bad charaters + // Spec: https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions + auto trimmed_version = version.trim_whitespace(); + for (auto const& code_point : trimmed_version.bytes()) { + if (is_ascii_space(code_point) || code_point == '_') { + return Error::from_string_view("Bad characters found in the version string"sv); + } + } + + GenericLexer lexer(trimmed_version); + if (lexer.tell_remaining() == 0) + return Error::from_string_view("Version string is empty"sv); + + // Parse the normal version parts. + // https://semver.org/#spec-item-2 + auto version_part = lexer.consume_until(normal_version_separator).to_number(); + if (!version_part.has_value()) + return Error::from_string_view("Major version is not numeric"sv); + auto version_major = version_part.value(); + + lexer.consume(); + + version_part = lexer.consume_until(normal_version_separator).to_number(); + if (!version_part.has_value()) + return Error::from_string_view("Minor version is not numeric"sv); + auto version_minor = version_part.value(); + + lexer.consume(); + + version_part = lexer.consume_while([](char ch) { return ch >= '0' && ch <= '9'; }).to_number(); + if (!version_part.has_value()) + return Error::from_string_view("Patch version is not numeric"sv); + auto version_patch = version_part.value(); + + if (lexer.is_eof()) + return SemVer(version_major, version_minor, version_patch, normal_version_separator); + + Vector build_metadata_identifiers; + Vector prerelease_identifiers; + + auto process_build_metadata = [&lexer, &build_metadata_identifiers]() -> ErrorOr { + // Function body strictly adheres to the spec + // Spec: https://semver.org/#spec-item-10 + if (lexer.is_eof()) { + return Error::from_string_view("Build metadata can't be empty"sv); + } + + auto build_metadata = TRY(String::from_utf8(lexer.consume_all())); + build_metadata_identifiers = TRY(build_metadata.split('.')); + + // Because there is no mention about leading zero in the spec, only empty check is used + for (auto& identifier : build_metadata_identifiers) { + if (identifier.is_empty()) { + return Error::from_string_view("Build metadata identifier must be non empty string"sv); + } + } + + return {}; + }; + + switch (lexer.consume()) { + case '+': { + // Build metadata always starts with the + symbol after normal version string. + TRY(process_build_metadata()); + break; + } + case '-': { + // Pre-releases always start with the - symbol after normal version string. + // Spec: https://semver.org/#spec-item-9 + if (lexer.is_eof()) + return Error::from_string_view("Pre-release can't be empty"sv); + + auto prerelease = TRY(String::from_utf8(lexer.consume_until('+'))); + + constexpr auto is_valid_identifier = [](String const& identifier) { + for (auto const& code_point : identifier.code_points()) { + if (!is_ascii_alphanumeric(code_point) && code_point != '-') { + return false; + } + } + return true; + }; + // Parts of prerelease (identitifers) are separated by dot (.) + prerelease_identifiers = TRY(prerelease.split('.')); + for (auto const& prerelease_identifier : prerelease_identifiers) { + // Empty identifiers are not allowed. + if (prerelease_identifier.is_empty()) + return Error::from_string_view("Prerelease identifier can't be empty"sv); + + // If there are multiple digits, it can't start with 0 digit. + // 1.2.3-0 or 1.2.3-0is.legal are valid, but not 1.2.3-00 or 1.2.3-01 + auto identifier_bytes = prerelease_identifier.bytes(); + if (identifier_bytes.size() > 1 && prerelease_identifier.starts_with('0') && is_ascii_digit(identifier_bytes[1])) + return Error::from_string_view("Prerelease identifier has leading redundant zeroes"sv); + + // Validate identifier against charset + if (!is_valid_identifier(prerelease_identifier)) + return Error::from_string_view("Characters in prerelease identifier must be either hyphen (-), dot (.) or alphanumeric"sv); + } + + if (!lexer.is_eof()) { + // This would invalidate the following versions. + // 1.2.3-pre$ss 1.2.3-pre.1.0*build-meta + if (lexer.consume() != '+') { + return Error::from_string_view("After processing pre-release, only + character is allowed for build metadata information"sv); + } + + // Process the pending build metadata information, ignoring invalids like following. + // 1.2.3-pre+ is not a valid version. + TRY(process_build_metadata()); + } + break; + } + default: + // TODO: Add context information like actual character (peek) and its index, use the following format. + // "Expected prerelease (-) or build metadata (+) character at {}. Found {}" + return Error::from_string_view("Malformed version syntax. Expected + or - characters"sv); + } + + return SemVer(version_major, version_minor, version_patch, normal_version_separator, prerelease_identifiers, build_metadata_identifiers); +} + +bool is_valid(StringView const& version, char normal_version_separator) +{ + auto result = from_string_view(version, normal_version_separator); + return !result.is_error() && result.release_value().to_string() == version; +} +} diff --git a/Userland/Libraries/LibSemVer/SemVer.h b/Userland/Libraries/LibSemVer/SemVer.h new file mode 100644 index 0000000000..15834a7f50 --- /dev/null +++ b/Userland/Libraries/LibSemVer/SemVer.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023, Gurkirat Singh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace SemVer { +enum class BumpType { + Major, + Minor, + Patch, + Prerelease, +}; + +enum class CompareType { + Exact, + Major, + Minor, + Patch +}; + +class SemVer { + +public: + SemVer(u64 major, u64 minor, u64 patch, char m_number_separator) + : m_number_separator(m_number_separator) + , m_major(major) + , m_minor(minor) + , m_patch(patch) + { + } + + SemVer(u64 major, u64 minor, u64 patch, char m_number_separator, Vector const& prereleases, Vector const& build_metadata) + : m_number_separator(m_number_separator) + , m_major(major) + , m_minor(minor) + , m_patch(patch) + , m_prerelease_identifiers(prereleases) + , m_build_metadata_identifiers(build_metadata) + { + } + + [[nodiscard]] u64 major() const { return m_major; } + [[nodiscard]] u64 minor() const { return m_minor; } + [[nodiscard]] u64 patch() const { return m_patch; } + [[nodiscard]] ReadonlySpan prerelease_identifiers() const { return m_prerelease_identifiers.span(); } + [[nodiscard]] String prerelease() const + { + return String::join('.', m_prerelease_identifiers).release_value_but_fixme_should_propagate_errors(); + } + [[nodiscard]] ReadonlySpan build_metadata_identifiers() const { return m_build_metadata_identifiers.span(); } + [[nodiscard]] String build_metadata() const { return String::join('.', m_build_metadata_identifiers).release_value_but_fixme_should_propagate_errors(); } + + [[nodiscard]] SemVer bump(BumpType) const; + + [[nodiscard]] bool is_same(SemVer const&, CompareType = CompareType::Exact) const; + [[nodiscard]] bool is_greater_than(SemVer const&) const; + [[nodiscard]] bool is_lesser_than(SemVer const& other) const { return !is_same(other) && !is_greater_than(other); } + [[nodiscard]] bool operator==(SemVer const& other) const { return is_same(other); } + [[nodiscard]] bool operator!=(SemVer const& other) const { return !is_same(other); } + [[nodiscard]] bool operator>(SemVer const& other) const { return is_lesser_than(other); } + [[nodiscard]] bool operator<(SemVer const& other) const { return is_greater_than(other); } + [[nodiscard]] bool operator>=(SemVer const& other) const { return *this == other || *this > other; } + [[nodiscard]] bool operator<=(SemVer const& other) const { return *this == other || *this < other; } + + [[nodiscard]] bool satisfies(StringView const& semver_spec) const; + + [[nodiscard]] String suffix() const; + [[nodiscard]] String to_string() const; + +private: + char m_number_separator; + u64 m_major; + u64 m_minor; + u64 m_patch; + Vector m_prerelease_identifiers; + Vector m_build_metadata_identifiers; +}; + +ErrorOr from_string_view(StringView const&, char normal_version_separator = '.'); + +bool is_valid(StringView const&, char normal_version_separator = '.'); + +} + +template<> +struct AK::Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, SemVer::SemVer const& value) + { + return Formatter::format(builder, value.to_string()); + } +};