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.

    <valid semver> ::= <version core>
                     | <version core> "-" <pre-release>
                     | <version core> "+" <build>
                     | <version core> "-" <pre-release> "+" <build>
This commit is contained in:
Gurkirat Singh 2023-12-21 15:49:11 +05:30 committed by Tim Schumacher
parent 14200de80b
commit ee639fa1df
8 changed files with 770 additions and 0 deletions

View file

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

View file

@ -0,0 +1,8 @@
set(TEST_SOURCES
TestFromStringView.cpp
TestSemVer.cpp
)
foreach(source IN LISTS TEST_SOURCES)
serenity_test("${source}" LibSemVer LIBS LibSemVer)
endforeach()

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2023, Gurkirat Singh <tbhaxor@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringView.h>
#include <AK/Tuple.h>
#include <AK/Vector.h>
#include <LibSemVer/SemVer.h>
#include <LibTest/TestCase.h>
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, '-'));
}

View file

@ -0,0 +1,248 @@
/*
* Copyright (c) 2023, Gurkirat Singh <tbhaxor@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringView.h>
#include <AK/Tuple.h>
#include <AK/Vector.h>
#include <LibSemVer/SemVer.h>
#include <LibTest/TestCase.h>
#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));
}

View file

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

View file

@ -0,0 +1,5 @@
set(SOURCES
SemVer.cpp
)
serenity_lib(LibSemVer semver)

View file

@ -0,0 +1,303 @@
/*
* Copyright (c) 2023, Gurkirat Singh <tbhaxor@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/CharacterTypes.h>
#include <AK/Find.h>
#include <AK/GenericLexer.h>
#include <AK/ReverseIterator.h>
#include <AK/StringBuilder.h>
#include <LibSemVer/SemVer.h>
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<String> 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<u32>();
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<u32>();
auto const other_numerical_identifier = other.m_prerelease_identifiers[i].to_number<u32>();
// 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<SemVer> 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<u64>();
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<u64>();
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<u64>();
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<String> build_metadata_identifiers;
Vector<String> prerelease_identifiers;
auto process_build_metadata = [&lexer, &build_metadata_identifiers]() -> ErrorOr<void> {
// 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;
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2023, Gurkirat Singh <tbhaxor@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/Format.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Types.h>
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<String> const& prereleases, Vector<String> 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<String> 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<String> 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<String> m_prerelease_identifiers;
Vector<String> m_build_metadata_identifiers;
};
ErrorOr<SemVer> from_string_view(StringView const&, char normal_version_separator = '.');
bool is_valid(StringView const&, char normal_version_separator = '.');
}
template<>
struct AK::Formatter<SemVer::SemVer> : Formatter<String> {
ErrorOr<void> format(FormatBuilder& builder, SemVer::SemVer const& value)
{
return Formatter<String>::format(builder, value.to_string());
}
};