Skip to content
Merged
58 changes: 35 additions & 23 deletions include/semver/semver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,29 @@ SOFTWARE.

namespace semver
{

using build_identifier_part = uint64_t;

const std::string default_prerelease_part = "0";
const char prerelease_delimiter = '.';
const std::string numbers = "0123456789";
const std::string prerelease_allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-";
const std::string version_pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
const std::string loose_version_pattern = "^v?(0|[1-9]\\d*)(?:\\.(0|[1-9]\\d*))?(?:\\.(0|[1-9]\\d*))?(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
const std::string version_pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we change regex? Where are these new patterns coming from?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the same regex, Oded just broke it into lines to make it easier to read the major.minor.patch parts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@logan is right, I just broke the regex into those parts as defined in standard.

"(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))"
Copy link
Author

@katzoded katzoded Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Twingate/sdwan,
maybe you can help me understand this regex and if it complies with RFC ABNF .

<pre-release identifier> ::= <alphanumeric identifier>
                           | <numeric identifier>

I would do something much easier on this one (?:-[a-zA-Z0-9\.]*), although I tried to modify it and it didn't go that easy for the tests!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually the regex it taken from RFC

"?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
const std::string loose_version_pattern = "^v?(0|[1-9]\\d*)(?:\\.(0|[1-9]\\d*))?(?:\\.(0|[1-9]\\d*))"
"?(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))"
"?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";

struct semver_exception : public std::runtime_error {
semver_exception(const std::string& message) : std::runtime_error(message) { }
};

inline build_identifier_part parse_build_identifier_part(const std::string& version_part)
{
return static_cast<build_identifier_part>(std::stoull(version_part));
}

inline std::vector<std::string> split(const std::string& text, const char& delimiter) {
std::size_t pos_start = 0, pos_end, delim_len = 1;
std::string current;
Expand Down Expand Up @@ -69,7 +81,7 @@ namespace semver
private:
bool m_numeric = false;
std::string m_value;
unsigned long m_numeric_value;
build_identifier_part m_numeric_value;
public:
prerelease_part(const std::string& part) {
if (part.empty()) {
Expand All @@ -81,7 +93,7 @@ namespace semver
throw semver_exception(
"Pre-release part '" + part + "' is numeric but contains a leading zero.");
}
m_numeric_value = std::stoul(part);
m_numeric_value = parse_build_identifier_part(part);
m_numeric = true;
}
if (!is_valid_prerelease(part)) {
Expand All @@ -93,7 +105,7 @@ namespace semver

bool numeric() const { return m_numeric; }
std::string value() const { return m_value; }
unsigned long numeric_value() const { return m_numeric_value; }
build_identifier_part numeric_value() const { return m_numeric_value; }

int compare(const prerelease_part& other) const {
if (m_numeric && !other.m_numeric) return -1;
Expand Down Expand Up @@ -193,9 +205,9 @@ namespace semver

class version {
private:
unsigned long m_major;
unsigned long m_minor;
unsigned long m_patch;
build_identifier_part m_major;
build_identifier_part m_minor;
build_identifier_part m_patch;
prerelease_descriptor m_prerelease;
std::string m_build_meta;

Expand All @@ -212,9 +224,9 @@ namespace semver
return 0;
}
public:
version(unsigned long major = 0,
unsigned long minor = 0,
unsigned long patch = 0,
version(build_identifier_part major = 0,
build_identifier_part minor = 0,
build_identifier_part patch = 0,
std::string prerelease = "",
std::string build_meta = "")
: m_major{major},
Expand All @@ -223,9 +235,9 @@ namespace semver
m_prerelease{prerelease_descriptor::parse(prerelease)},
m_build_meta{build_meta} { }

unsigned long major() const { return m_major; }
unsigned long minor() const { return m_minor; }
unsigned long patch() const { return m_patch; }
build_identifier_part major() const { return m_major; }
build_identifier_part minor() const { return m_minor; }
build_identifier_part patch() const { return m_patch; }
std::string prerelease() const { return m_prerelease.str(); }
std::string build_meta() const { return m_build_meta; }

Expand Down Expand Up @@ -302,9 +314,9 @@ namespace semver
static version parse(const std::string& version_str, bool strict = true) {
std::regex regex(strict ? version_pattern : loose_version_pattern);
std::cmatch match;
unsigned long major;
unsigned long minor;
unsigned long patch;
build_identifier_part major;
build_identifier_part minor;
build_identifier_part patch;
std::string prerelease = "";
std::string build_meta = "";

Expand All @@ -323,13 +335,13 @@ namespace semver

try {
if (strict && major_m.matched && minor_m.matched && patch_m.matched) {
major = std::stoul(major_m);
minor = std::stoul(minor_m);
patch = std::stoul(patch_m);
major = parse_build_identifier_part(major_m);
minor = parse_build_identifier_part(minor_m);
patch = parse_build_identifier_part(patch_m);
} else if (!strict && major_m.matched) {
major = std::stoul(major_m);
minor = minor_m.matched ? std::stoul(minor_m) : 0;
patch = patch_m.matched ? std::stoul(patch_m) : 0;
major = parse_build_identifier_part(major_m);
minor = minor_m.matched ? parse_build_identifier_part(minor_m) : 0;
patch = patch_m.matched ? parse_build_identifier_part(patch_m) : 0;
} else {
throw semver_exception("Invalid version: " + version_str);
}
Expand Down
1 change: 1 addition & 0 deletions test/version.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ TEST_CASE("Test version strings", "[version]") {
REQUIRE(semver::version::parse("1", false).str() == "1.0.0");
REQUIRE(semver::version::parse("1.2", false).str() == "1.2.0");
REQUIRE(semver::version::parse("v1.2", false).str() == "1.2.0");
REQUIRE(semver::version::parse("18446744073709551615.18446744073709551614.18446744073709551613").str() == "18446744073709551615.18446744073709551614.18446744073709551613");

REQUIRE(semver::version::parse("v1.2.3-alpha+build", false).str() == "1.2.3-alpha+build");
REQUIRE(semver::version::parse("v1-alpha+build", false).str() == "1.0.0-alpha+build");
Expand Down