From 8561af5a2551ec453c9888125de273f2cc2940c0 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 14 Aug 2018 14:09:32 +0200 Subject: Add support for parsing semantic and semantic-like versions --- libbutl/semantic-version.cxx | 150 ++++++++++++++++++++++++++++++ libbutl/semantic-version.ixx | 78 ++++++++++++++++ libbutl/semantic-version.mxx | 187 ++++++++++++++++++++++++++++++++++++++ libbutl/standard-version.cxx | 13 ++- libbutl/standard-version.mxx | 19 ++++ tests/semantic-version/buildfile | 8 ++ tests/semantic-version/driver.cxx | 84 +++++++++++++++++ 7 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 libbutl/semantic-version.cxx create mode 100644 libbutl/semantic-version.ixx create mode 100644 libbutl/semantic-version.mxx create mode 100644 tests/semantic-version/buildfile create mode 100644 tests/semantic-version/driver.cxx diff --git a/libbutl/semantic-version.cxx b/libbutl/semantic-version.cxx new file mode 100644 index 0000000..2427202 --- /dev/null +++ b/libbutl/semantic-version.cxx @@ -0,0 +1,150 @@ +// file : libbutl/semantic-version.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef __cpp_modules +#include +#endif + +#include + +#ifndef __cpp_lib_modules +#include +#include +#include +#include + +#include // strchr() +#include // strtoull() +#include // move() +#include // invalid_argument +#endif + +// Other includes. + +#ifdef __cpp_modules +module butl.semantic_version; + +// Only imports additional to interface. +#ifdef __clang__ +#ifdef __cpp_lib_modules +import std.core; +import std.io; +#endif +import butl.optional; +#endif +#else +#endif + +using namespace std; + +namespace butl +{ + string semantic_version:: + string (bool ib) const + { + std::string r; + r = to_string (major); + r += '.'; + r += to_string (minor); + r += '.'; + r += to_string (patch); + if (!ib) r += build; + return r; + } + + uint64_t semantic_version:: + numeric () const + { + if (const char* w = (major > 999 ? "major version greater than 999" : + minor > 999 ? "minor version greater than 999" : + patch > 999 ? "patch version greater than 999" : + nullptr)) + throw invalid_argument (w); + + // AAABBBCCC0000 + // 10000000000 + // 10000000 + // 10000 + // + return (major * 10000000000) + (minor * 10000000) + (patch * 10000); + } + + semantic_version:: + semantic_version (uint64_t n, std::string b) + : build (move (b)) + { + // AAABBBCCC0000 + if (n > 9999999990000ULL || (n % 1000) != 0) + throw invalid_argument ("invalid numeric representation"); + + // AAABBBCCC0000 + major = n / 10000000000 % 1000; + minor = n / 10000000 % 1000; + patch = n / 10000 % 1000; + } + + semantic_version:: + semantic_version (const std::string& s, size_t p, const char* bs) + { + semantic_version_result r (parse_semantic_version_impl (s, p, bs)); + + if (r.version) + *this = move (*r.version); + else + throw invalid_argument (r.failure_reason); + } + + // From standard-version.cxx + // + bool + parse_uint64 (const string& s, size_t& p, + uint64_t& r, + uint64_t min = 0, uint64_t max = uint64_t (~0)); + + semantic_version_result + parse_semantic_version_impl (const string& s, size_t p, const char* bs) + { + auto bail = [] (string m) + { + return semantic_version_result {nullopt, move (m)}; + }; + + semantic_version r; + + if (!parse_uint64 (s, p, r.major)) + return bail ("invalid major version"); + + if (s[p] != '.') + return bail ("'.' expected after major version"); + + if (!parse_uint64 (s, ++p, r.minor)) + return bail ("invalid minor version"); + + if (s[p] == '.') + { + // Treat it as build if failed to parse as patch (e.g., 1.2.alpha). + // + if (!parse_uint64 (s, ++p, r.patch)) + { + //if (require_patch) + // return bail ("invalid patch version"); + + --p; + // Fall through. + } + } + //else if (require_patch) + // return bail ("'.' expected after minor version"); + + if (char c = s[p]) + { + if (bs == nullptr || (*bs != '\0' && strchr (bs, c) == nullptr)) + return bail ("junk after version"); + + r.build.assign (s, p, string::npos); + } + + return semantic_version_result {move (r), string ()}; + } +} diff --git a/libbutl/semantic-version.ixx b/libbutl/semantic-version.ixx new file mode 100644 index 0000000..cd88663 --- /dev/null +++ b/libbutl/semantic-version.ixx @@ -0,0 +1,78 @@ +// file : libbutl/semantic-version.ixx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +namespace butl +{ + inline semantic_version:: + semantic_version (std::uint64_t mj, + std::uint64_t mi, + std::uint64_t p, + std::string b) + : major (mj), + minor (mi), + patch (p), + build (std::move (b)) + { + } + + inline semantic_version:: + semantic_version (const std::string& s, bool ab) + : semantic_version (s, ab ? "-+" : nullptr) + { + } + + inline semantic_version:: + semantic_version (const std::string& s, const char* bs) + : semantic_version (s, 0, bs) + { + } + + inline semantic_version:: + semantic_version (const std::string& s, std::size_t p, bool ab) + : semantic_version (s, p, ab ? "-+" : nullptr) + { + } + + inline int semantic_version:: + compare (const semantic_version& v, bool ib) const + { + return (major != v.major ? (major < v.major ? -1 : 1) : + minor != v.minor ? (minor < v.minor ? -1 : 1) : + patch != v.patch ? (patch < v.patch ? -1 : 1) : + ib ? 0 : build.compare (v.build)); + } + + struct semantic_version_result + { + optional version; + std::string failure_reason; + }; + + LIBBUTL_SYMEXPORT semantic_version_result + parse_semantic_version_impl (const std::string&, std::size_t, const char*); + + inline optional + parse_semantic_version (const std::string& s, bool ab) + { + return parse_semantic_version (s, ab ? "-+" : nullptr); + } + + inline optional + parse_semantic_version (const std::string& s, const char* bs) + { + return parse_semantic_version_impl (s, 0, bs).version; + } + + inline optional + parse_semantic_version (const std::string& s, std::size_t p, bool ab) + { + return parse_semantic_version (s, p, ab ? "-+" : nullptr); + } + + inline optional + parse_semantic_version (const std::string& s, std::size_t p, const char* bs) + { + return parse_semantic_version_impl (s, p, bs).version; + } +} diff --git a/libbutl/semantic-version.mxx b/libbutl/semantic-version.mxx new file mode 100644 index 0000000..d64eb82 --- /dev/null +++ b/libbutl/semantic-version.mxx @@ -0,0 +1,187 @@ +// file : libbutl/semantic-version.mxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef __cpp_modules +#pragma once +#endif + +// C includes. + +#ifndef __cpp_lib_modules +#include +#include // size_t +#include // uint*_t +#include // move() +#include +#endif + +// Other includes. + +#ifdef __cpp_modules +export module butl.semantic_version; +#ifdef __cpp_lib_modules +import std.core; +import std.io; +#endif +import butl.optional; +#else +#include +#endif + +#include + +// FreeBSD defines these macros in its . +// +#ifdef major +# undef major +#endif + +#ifdef minor +# undef minor +#endif + +LIBBUTL_MODEXPORT namespace butl +{ + // Semantic or semantic-like version. + // + // .[.][] + // + // If the patch component is absent, then it defaults to 0. + // + // @@ Currently there is no way to enforce the three-component version. + // Supporting this will require changing allow_build to a bit-wise + // flag. See parse_semantic_version_impl() for some sketched code. + // We may also want to pass these flags to string() to not print + // 0 patch. + // + // By default, a version containing the component is considered + // valid only if separated from with '-' (semver pre-release) or '+' + // (semver build metadata). However, as discussed below, the list of valid + // separators can be customized to recognize other semver-like formats. + // + // Note also that the format of semver pre-release and build metadata are + // not validated. + // + struct LIBBUTL_SYMEXPORT semantic_version + { + std::uint64_t major = 0; + std::uint64_t minor = 0; + std::uint64_t patch = 0; + std::string build; + + // Construct the semantic version from various representations. Throw + // std::invalid_argument if the format is not recognizable or components + // are invalid. + // + semantic_version () = default; + + semantic_version (std::uint64_t major, + std::uint64_t minor, + std::uint64_t patch, + std::string build = ""); + + // The build_separators argument can be NULL (no build component allowed), + // empty (any build component allowed), or a string of characters to allow + // as separators. When allow_build is true build_separators defaults to + // "-+". + // + explicit + semantic_version (const std::string&, bool allow_build = true); + + semantic_version (const std::string&, const char* build_separators); + + // As above but parse from the specified position until the end of the + // string. + // + semantic_version (const std::string&, std::size_t pos, bool = true); + + semantic_version (const std::string&, std::size_t pos, const char*); + + std::string + string (bool ignore_build = false) const; + + // Numeric representation in the AAABBBCCC0000 form, where: + // + // AAA - major version number + // BBB - minor version number + // CCC - patch version number + // + // See standard version for details. + // + explicit + semantic_version (std::uint64_t numeric, std::string build = ""); + + // If any of the major/minor/patch components is greater than 999, then + // throw std::invalid_argument. The build component is ignored. + // + std::uint64_t + numeric () const; + + // Unless instructed to ignore, the build components are compared + // lexicographically. + // + int + compare (const semantic_version&, bool ignore_build = false) const; + }; + + // Try to parse a string as a semantic version returning nullopt if invalid. + // + optional + parse_semantic_version (const std::string&, bool allow_build = true); + + optional + parse_semantic_version (const std::string&, const char* build_separators); + + optional + parse_semantic_version (const std::string&, std::size_t pos, bool = true); + + optional + parse_semantic_version (const std::string&, std::size_t pos, const char*); + + // NOTE: comparison operators take the build component into account. + // + inline bool + operator< (const semantic_version& x, const semantic_version& y) noexcept + { + return x.compare (y) < 0; + } + + inline bool + operator> (const semantic_version& x, const semantic_version& y) noexcept + { + return x.compare (y) > 0; + } + + inline bool + operator== (const semantic_version& x, const semantic_version& y) noexcept + { + return x.compare (y) == 0; + } + + inline bool + operator<= (const semantic_version& x, const semantic_version& y) noexcept + { + return x.compare (y) <= 0; + } + + inline bool + operator>= (const semantic_version& x, const semantic_version& y) noexcept + { + return x.compare (y) >= 0; + } + + inline bool + operator!= (const semantic_version& x, const semantic_version& y) noexcept + { + return !(x == y); + } + + inline std::ostream& + operator<< (std::ostream& o, const semantic_version& x) + { + return o << x.string (); + } +} + +#include diff --git a/libbutl/standard-version.cxx b/libbutl/standard-version.cxx index e4180d1..fc2d528 100644 --- a/libbutl/standard-version.cxx +++ b/libbutl/standard-version.cxx @@ -42,12 +42,17 @@ using namespace std; namespace butl { - // Utility functions + // Parse uint64_t from the specified string starting at the specified + // position and check the min/max constraints. If successful, save the + // result, update the position to point to the next character, and return + // true. Otherwise return false (result and position are unchanged). // - static bool + // Note: also used for semantic_version parsing. + // + bool parse_uint64 (const string& s, size_t& p, uint64_t& r, - uint64_t min, uint64_t max) + uint64_t min = 0, uint64_t max = uint64_t (~0)) { if (s[p] == '-' || s[p] == '+') // strtoull() allows these. return false; @@ -59,8 +64,8 @@ namespace butl if (errno == ERANGE || b == e || v < min || v > max) return false; + r = v; p = e - s.c_str (); - r = static_cast (v); return true; } diff --git a/libbutl/standard-version.mxx b/libbutl/standard-version.mxx index 5f019c4..7b1f232 100644 --- a/libbutl/standard-version.mxx +++ b/libbutl/standard-version.mxx @@ -48,6 +48,25 @@ LIBBUTL_MODEXPORT namespace butl // [+-]..- // 0[+] // + // The numeric version format is AAABBBCCCDDDE where: + // + // AAA - major version number + // BBB - minor version number + // CCC - patch version number + // DDD - alpha / beta (DDD + 500) version number + // E - final (0) / snapshot (1) + // + // When DDDE is not 0, 1 is subtracted from AAABBBCCC. For example: + // + // Version AAABBBCCCDDDE + // + // 0.1.0 0000010000000 + // 0.1.2 0000010010000 + // 1.2.3 0010020030000 + // 2.2.0-a.1 0020019990010 + // 3.0.0-b.2 0029999995020 + // 2.2.0-a.1.z 0020019990011 + // struct LIBBUTL_SYMEXPORT standard_version { // Invariants: diff --git a/tests/semantic-version/buildfile b/tests/semantic-version/buildfile new file mode 100644 index 0000000..1055228 --- /dev/null +++ b/tests/semantic-version/buildfile @@ -0,0 +1,8 @@ +# file : tests/semantic-version/buildfile +# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} +libs += $stdmod_lib + +exe{driver}: {hxx cxx}{*} $libs diff --git a/tests/semantic-version/driver.cxx b/tests/semantic-version/driver.cxx new file mode 100644 index 0000000..024e7a0 --- /dev/null +++ b/tests/semantic-version/driver.cxx @@ -0,0 +1,84 @@ +// file : tests/semantic-version/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#ifndef __cpp_lib_modules +#include +#endif + +// Other includes. + +#ifdef __cpp_modules +#ifdef __cpp_lib_modules +import std.core; +import std.io; +#endif +import butl.semantic_version; +#else +#include +#endif + +using namespace std; +using namespace butl; + +int +main () +{ + using semver = semantic_version; + using failed = invalid_argument; + + // Construction. + // + { + semver v; + assert (v.major == 0 && v.minor == 0 && v.patch == 0 && v.build.empty ()); + } + + { + semver v (1, 2, 3); + assert (v.major == 1 && v.minor == 2 && v.patch == 3 && v.build.empty ()); + assert (v.string () == "1.2.3"); + } + { + semver v (1, 2, 3, ".4"); + assert (v.major == 1 && v.minor == 2 && v.patch == 3 && v.build == ".4"); + assert (v.string () == "1.2.3.4"); + assert (v.string (true) == "1.2.3"); + } + + // Comparison. + // + assert (semver (2, 0 ,0) > semver (1, 2, 3)); + assert (semver (1, 2, 0) > semver (1, 1, 2)); + assert (semver (1, 1, 2) > semver (1, 1, 1, ".2")); + assert (semver (1, 1, 1, ".2") > semver (1, 1, 1, ".1")); + assert (semver (1, 1, 1, ".1").compare (semver (1, 1, 1, ".2"), true) == 0); + + // String representation. + // + assert (semver ("1.2") == semver (1, 2, 0)); + assert (semver ("1.2-3") == semver (1, 2, 0, "-3")); + assert (semver ("1.2.a1", "+-.") == semver (1, 2, 0, ".a1")); + assert (semver ("1.2.3") == semver (1, 2, 3)); + assert (semver ("1.2.3-4") == semver (1, 2, 3, "-4")); + assert (semver ("1.2.3+4") == semver (1, 2, 3, "+4")); + assert (semver ("1.2.3.4", "+-.") == semver (1, 2, 3, ".4")); + assert (semver ("1.2.3a", "") == semver (1, 2, 3, "a")); + try {semver v ("1.2.3-4", false); assert (false);} catch (failed) {} + try {semver v ("1.2.3.4"); assert (false);} catch (failed) {} + try {semver v ("1.2.3a"); assert (false);} catch (failed) {} + assert (!parse_semantic_version ("1.2.3.4")); + + // Numeric representation. + // + // AAABBBCCC0000 + assert (semver ( 10020030000ULL) == semver (1, 2, 3)); + assert (semver ( 9999999990000ULL, ".4") == semver (999, 999, 999, ".4")); + try { semver v ( 10020030001ULL); assert (false);} catch (failed) {} + try { semver v (10000020030000ULL); assert (false);} catch (failed) {} + assert (semver (1, 2, 3).numeric () == 10020030000ULL); + assert (semver (999, 999, 999, ".4").numeric () == 9999999990000ULL); + try { semver (9999, 0, 0).numeric (); assert (false);} catch (failed) {} +} -- cgit v1.1