From 1bea889fd59b4ac3a32232e8f7a9ba34506717dc Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 25 Apr 2017 11:53:11 +0200 Subject: Add standard_version class --- butl/buildfile | 1 + butl/standard-version | 111 +++++++++++++ butl/standard-version.cxx | 333 ++++++++++++++++++++++++++++++++++++++ butl/standard-version.ixx | 94 +++++++++++ tests/standard-version/buildfile | 7 + tests/standard-version/driver.cxx | 136 ++++++++++++++++ tests/standard-version/testscript | 190 ++++++++++++++++++++++ 7 files changed, 872 insertions(+) create mode 100644 butl/standard-version create mode 100644 butl/standard-version.cxx create mode 100644 butl/standard-version.ixx create mode 100644 tests/standard-version/buildfile create mode 100644 tests/standard-version/driver.cxx create mode 100644 tests/standard-version/testscript diff --git a/butl/buildfile b/butl/buildfile index 75217fa..04e6e09 100644 --- a/butl/buildfile +++ b/butl/buildfile @@ -28,6 +28,7 @@ lib{butl}: \ {hxx ixx cxx}{ sendmail } \ {hxx cxx}{ sha256 } \ {hxx }{ small-vector } \ + {hxx ixx cxx}{ standard-version } \ {hxx cxx}{ string-parser } \ {hxx txx }{ string-table } \ {hxx cxx}{ tab-parser } \ diff --git a/butl/standard-version b/butl/standard-version new file mode 100644 index 0000000..0888900 --- /dev/null +++ b/butl/standard-version @@ -0,0 +1,111 @@ +// file : butl/standard-version -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUTL_STANDARD_VERSION +#define BUTL_STANDARD_VERSION + +#include +#include // uint*_t +#include // size_t +#include + +#include + +namespace butl +{ + // The build2 "standard version": + // + // [~]..[-(a|b).[.[.]]][+] + // + struct LIBBUTL_EXPORT standard_version + { + // Invariants: + // + // 1. (E == 0) == (snapshot_sn == 0 && snapshot_id.empty ()) + // + // 2. snapshot_sn != latest_sn || snapshot_id.empty () + // + static const std::uint64_t latest_sn = std::uint64_t (~0); + + std::uint16_t epoch = 0; // 0 if not specified. + std::uint64_t version = 0; // AAABBBCCCDDDE + std::uint64_t snapshot_sn = 0; // 0 if not specifed, latest_sn if 'z'. + std::string snapshot_id; // Empty if not specified. + std::uint16_t revision = 0; // 0 if not specified. + + std::uint16_t major () const; + std::uint16_t minor () const; + std::uint16_t patch () const; + std::uint16_t pre_release () const; // Note: 0 is ambiguous (-a.0.z). + + // Note: return empty if the corresponding component is unspecified. + // + std::string string () const; // Package version. + std::string string_project () const; // Project version (no epoch/rev). + std::string string_version () const; // Version only (no snapshot). + std::string string_pre_release () const; // Pre-release part only (a.1). + std::string string_snapshot () const; // Snapshot part only (1234.1f23). + + bool empty () const {return version == 0;} + + bool alpha () const; + bool beta () const; + bool snapshot () const {return snapshot_sn != 0;} + + int + compare (const standard_version&) const; + + // Parse the version. Throw std::invalid_argument if the format is not + // recognizable or components are invalid. + // + explicit + standard_version (const std::string&); + + explicit + standard_version (std::uint64_t version); + + standard_version (std::uint64_t version, const std::string& snapshot); + + standard_version (std::uint16_t epoch, + std::uint64_t version, + const std::string& snapshot, + std::uint16_t revision); + + standard_version (std::uint16_t epoch, + std::uint64_t version, + std::uint64_t snapshot_sn, + std::string snapshot_id, + std::uint16_t revision); + + // Create empty version. + // + standard_version () = default; + + private: + void + parse_snapshot (const std::string&, std::size_t&); + }; + + inline bool + operator== (const standard_version& x, const standard_version& y) + { + return x.compare (y) == 0; + } + + inline bool + operator!= (const standard_version& x, const standard_version& y) + { + return !(x == y); + } + + inline std::ostream& + operator<< (std::ostream& o, const standard_version& x) + { + return o << x.string (); + } +} + +#include + +#endif // BUTL_STANDARD_VERSION diff --git a/butl/standard-version.cxx b/butl/standard-version.cxx new file mode 100644 index 0000000..7e144a6 --- /dev/null +++ b/butl/standard-version.cxx @@ -0,0 +1,333 @@ +// file : butl/standard-version.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include // strtoull() +#include // size_t +#include // invalid_argument + +#include // alnum() + +using namespace std; + +namespace butl +{ + // Utility functions + // + static uint64_t + parse_num (const string& s, size_t& p, + const char* m, + uint64_t min = 0, uint64_t max = 999) + { + if (s[p] == '-' || s[p] == '+') // strtoull() allows these. + throw invalid_argument (m); + + const char* b (s.c_str () + p); + char* e (nullptr); + uint64_t r (strtoull (b, &e, 10)); + + if (b == e || r < min || r > max) + throw invalid_argument (m); + + p = e - s.c_str (); + return static_cast (r); + } + + static void + check_version (uint64_t version, bool snapshot) + { + // Check that the version isn't too large. + // + // AAABBBCCCDDDE + bool r (version < 10000000000000ULL); + + // Check that E version component is consistent with the snapshot flag. + // + if (r) + r = (version % 10) == (snapshot ? 1 : 0); + + // Check that pre-release number is consistent with the snapshot flag. + // + if (r) + { + uint64_t ab (version / 10 % 1000); + + // Note that if ab is 0, it can either mean non-pre-release version in + // the absence of snapshot number, or 'a.0' pre-release otherwise. If ab + // is 500, it can only mean 'b.0', that must be followed by a snapshot + // number. + // + if (ab != 0) + r = ab != 500 || snapshot; + } + + // Check that the major, the minor and the bugfix versions are not + // simultaneously zeros. + // + if (r) + r = (version / 10000) != 0; + + if (!r) + throw invalid_argument ("invalid project version"); + } + + // standard_version + // + standard_version:: + standard_version (const std::string& s) + { + auto bail = [] (const char* m) {throw invalid_argument (m);}; + + // Pre-parse the first component to see if the version starts with epoch, + // to keep the subsequent parsing straightforward. + // + bool ep (false); + { + char* e (nullptr); + strtoull (s.c_str (), &e, 10); + ep = *e == '~'; + } + + size_t p (0), n (s.size ()); + + if (ep) + { + epoch = parse_num (s, p, "invalid epoch", 1, uint16_t (~0)); + ++p; // Skip '~'. + } + + uint16_t ma, mi, bf, ab (0); + + ma = parse_num (s, p, "invalid major version"); + + // Note that here and below p is less or equal n, and so s[p] is always + // valid. + // + if (s[p] != '.') + bail ("'.' expected after major version"); + + mi = parse_num (s, ++p, "invalid minor version"); + + if (s[p] != '.') + bail ("'.' expected after minor version"); + + bf = parse_num (s, ++p, "invalid bugfix version"); + + // AAABBBCCCDDDE + version = ma * 10000000000ULL + + mi * 10000000ULL + + bf * 10000ULL; + + if (version == 0) + bail ("0.0.0 version"); + + // Parse the pre-release component if present. + // + if (s[p] == '-') + { + char k (s[++p]); + + if (k != 'a' && k != 'b') + bail ("'a' or 'b' expected in pre-release"); + + if (s[++p] != '.') + bail ("'.' expected after pre-release letter"); + + ab = parse_num (s, ++p, "invalid pre-release", 0, 499); + + if (k == 'b') + ab += 500; + + // Parse the snapshot components if present. Note that pre-release number + // can't be zero for the final pre-release. + // + if (s[p] == '.') + parse_snapshot (s, ++p); + else if (ab == 0 || ab == 500) + bail ("invalid final pre-release"); + } + + if (s[p] == '+') + revision = parse_num (s, ++p, "invalid revision", 1, uint16_t (~0)); + + if (p != n) + bail ("junk after version"); + + if (ab != 0) + version -= 10000 - ab * 10; + + if (snapshot_sn != 0) + version += 1; + } + + standard_version:: + standard_version (uint64_t v) + : version (v) + { + check_version (v, false); + } + + standard_version:: + standard_version (uint64_t v, const std::string& s) + : version (v) + { + bool snapshot (!s.empty ()); + check_version (version, snapshot); + + if (snapshot) + { + size_t p (0); + parse_snapshot (s, p); + + if (p != s.size ()) + throw invalid_argument ("junk after snapshot"); + } + } + + standard_version:: + standard_version (uint16_t ep, + uint64_t vr, + uint64_t sn, + std::string si, + uint16_t rv) + : epoch (ep), + version (vr), + snapshot_sn (sn), + snapshot_id (move (si)), + revision (rv) + { + check_version (vr, true); + + if (!snapshot_id.empty () && (snapshot_id.size () > 16 || + snapshot_sn == 0 || + snapshot_sn == latest_sn)) + throw invalid_argument ("invalid snapshot"); + } + + void standard_version:: + parse_snapshot (const std::string& s, size_t& p) + { + // Note that snapshot id must be empty for 'z' snapshot number. + // + if (s[p] == 'z') + { + snapshot_sn = latest_sn; + ++p; + return; + } + + uint64_t sn (parse_num (s, + p, + "invalid snapshot number", + 1, latest_sn - 1)); + std::string id; + if (s[p] == '.') + { + char c; + for (++p; alnum (c = s[p]); ++p) + id += c; + + if (id.empty () || id.size () > 16) + throw invalid_argument ("invalid snapshot id"); + } + + snapshot_sn = sn; + snapshot_id = move (id); + } + + string standard_version:: + string_pre_release () const + { + std::string r; + + if (alpha () || beta ()) + { + uint64_t ab (version / 10 % 1000); + + if (ab < 500) + { + r += "a."; + r += to_string (ab); + } + else + { + r += "b."; + r += to_string (ab - 500); + } + } + + return r; + } + + string standard_version:: + string_version () const + { + std::string r (to_string (major ()) + '.' + to_string (minor ()) + '.' + + to_string (patch ())); + + if (alpha () || beta ()) + { + r += '-'; + r += string_pre_release (); + + if (snapshot ()) + r += '.'; + } + + return r; + } + + string standard_version:: + string_snapshot () const + { + std::string r; + + if (snapshot ()) + { + r = snapshot_sn == latest_sn ? "z" : to_string (snapshot_sn); + + if (!snapshot_id.empty ()) + { + r += '.'; + r += snapshot_id; + } + } + + return r; + } + + string standard_version:: + string_project () const + { + std::string r (string_version ()); + + if (snapshot ()) + r += string_snapshot (); // string_version() includes trailing dot. + + return r; + } + + string standard_version:: + string () const + { + std::string r; + + if (epoch != 0) + { + r = to_string (epoch); + r += '~'; + } + + r += string_project (); + + if (revision != 0) + { + r += '+'; + r += to_string (revision); + } + + return r; + } +} diff --git a/butl/standard-version.ixx b/butl/standard-version.ixx new file mode 100644 index 0000000..e414deb --- /dev/null +++ b/butl/standard-version.ixx @@ -0,0 +1,94 @@ +// file : butl/standard-version.ixx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +namespace butl +{ + inline standard_version:: + standard_version ( std::uint16_t e, + std::uint64_t v, + const std::string& s, + std::uint16_t r) + : standard_version (v, s) + { + // Can't initialize above due to ctor delegating. + // + epoch = e; + revision = r; + } + + inline std::uint16_t standard_version:: + major () const + { + std::uint64_t v (version / 10); + std::uint64_t ab (v % 1000); + if (ab != 0) + v += 1000 - ab; + + return static_cast (v / 1000000000 % 1000); + } + + inline std::uint16_t standard_version:: + minor () const + { + std::uint64_t v (version / 10); + std::uint64_t ab (v % 1000); + if (ab != 0) + v += 1000 - ab; + + return static_cast (v / 1000000 % 1000); + } + + inline std::uint16_t standard_version:: + patch () const + { + std::uint64_t v (version / 10); + std::uint64_t ab (v % 1000); + if (ab != 0) + v += 1000 - ab; + + return static_cast (v / 1000 % 1000); + } + + inline std::uint16_t standard_version:: + pre_release () const + { + std::uint64_t ab (version / 10 % 1000); + if (ab > 500) + ab -= 500; + + return static_cast (ab); + } + + inline bool standard_version:: + alpha () const + { + std::uint64_t abe (version % 10000); + return abe > 0 && abe < 5000; + } + + inline bool standard_version:: + beta () const + { + std::uint64_t abe (version % 10000); + return abe > 5000; + } + + inline int standard_version:: + compare (const standard_version& v) const + { + if (epoch != v.epoch) + return epoch < v.epoch ? -1 : 1; + + if (version != v.version) + return version < v.version ? -1 : 1; + + if (snapshot_sn != v.snapshot_sn) + return snapshot_sn < v.snapshot_sn ? -1 : 1; + + if (revision != v.revision) + return revision < v.revision ? -1 : 1; + + return 0; + } +} diff --git a/tests/standard-version/buildfile b/tests/standard-version/buildfile new file mode 100644 index 0000000..4afb691 --- /dev/null +++ b/tests/standard-version/buildfile @@ -0,0 +1,7 @@ +# file : tests/tab-parser/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +exe{driver}: cxx{driver} ../../butl/lib{butl} test{testscript} + +include ../../butl/ diff --git a/tests/standard-version/driver.cxx b/tests/standard-version/driver.cxx new file mode 100644 index 0000000..1299090 --- /dev/null +++ b/tests/standard-version/driver.cxx @@ -0,0 +1,136 @@ +// file : tests/standard-version/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include // ios::failbit, ios::badbit +#include +#include +#include +#include // invalid_argument + +#include // operator<<(ostream,exception) +#include + +using namespace std; +using namespace butl; + +// Create standard version from string, and also test another ctors. +// +static standard_version +version (const string& s) +{ + standard_version r (s); + + try + { + standard_version v (r.epoch, + r.version, + r.snapshot () + ? r.string_snapshot () + : string (), + r.revision); + + assert (r == v); + + if (r.epoch == 0 && r.revision == 0) + { + standard_version v (r.version, + r.snapshot () + ? r.string_snapshot () + : string ()); + assert (r == v); + + if (!r.snapshot ()) + { + standard_version v (r.version); + assert (r == v); + } + } + + if (r.snapshot ()) + { + standard_version v (r.epoch, + r.version, + r.snapshot_sn, + r.snapshot_id, + r.revision); + assert (r == v); + } + + } + catch (const invalid_argument& e) + { + cerr << e << endl; + assert (false); + } + + return r; +} + +// Usages: +// +// argv[0] -a +// argv[0] -b +// argv[0] -c +// argv[0] +// +// -a output 'y' for alpha-version, 'n' otherwise +// -b output 'y' for beta-version, 'n' otherwise +// -c output 0 if versions are equal, -1 if the first one is less, 1 otherwise +// +// If no options are specified, then create versions from STDIN lines, and +// print them to STDOUT. +// +int +main (int argc, char* argv[]) +try +{ + cin.exceptions (ios::badbit); + cout.exceptions (ios::failbit | ios::badbit); + + if (argc > 1) + { + string o (argv[1]); + + if (o == "-a") + { + assert (argc == 3); + char r (version (argv[2]).alpha () + ? 'y' + : 'n'); + + cout << r << endl; + } + else if (o == "-b") + { + assert (argc == 3); + char r (version (argv[2]).beta () + ? 'y' + : 'n'); + + cout << r << endl; + } + else if (o == "-c") + { + assert (argc == 4); + + int r (version (argv[2]).compare (version (argv[3]))); + cout << r << endl; + } + else + assert (false); + + return 0; + } + + string s; + while (getline (cin, s)) + cout << version (s) << endl; + + return 0; +} +catch (const invalid_argument& e) +{ + cerr << e << endl; + return 1; +} diff --git a/tests/standard-version/testscript b/tests/standard-version/testscript new file mode 100644 index 0000000..335bed9 --- /dev/null +++ b/tests/standard-version/testscript @@ -0,0 +1,190 @@ +# file : tests/standard-version/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +: valid +: +: Roundtrip version. +: +{ + : non-prerelease + : + $* <>EOF + 1.2.3 + EOF + + : prerelease + : + { + : final + : + $* <>EOF + 1.2.3-a.1 + 1.2.3-b.1 + EOF + + : snapshot + : + $* <>EOF + 1.2.3-a.1.z + 1.2.3-a.0.456 + 1.2.3-a.1.456.340c0a26a5ef + EOF + } + + : revision + : + $* <>EOF + 1.2.3+4 + 1.2.3-a.4+5 + 1.2.3-a.4.z+5 + 1.2.3-a.4.567+8 + 1.2.3-a.4.567.340c0a26a5ef+8 + EOF + + : epoch + : + $* <>EOF + 4~1.2.3 + EOF +} + +: invalid +: +{ + : major + : + $* <'a' 2>'invalid major version' == 1 + + : no-major-dot + : + $* <'1' 2>"'.' expected after major version" == 1 + + : minor + : + $* <'1.a' 2>'invalid minor version' == 1 + + : no-minor-dot + : + $* <'1.2' 2>"'.' expected after minor version" == 1 + + : bugfix + : + $* <'1.2.a' 2>'invalid bugfix version' == 1 + + : zero-version + : + $* <'1~0.0.0' 2>'0.0.0 version' == 1 + + : a-b-expected1 + : + $* <'1.2.3-' 2>"'a' or 'b' expected in pre-release" == 1 + + : a-b-expected2 + : + $* <'1.2.3-k' 2>"'a' or 'b' expected in pre-release" == 1 + + : prerelease-dot-expected + : + $* <'1.2.3-a' 2>"'.' expected after pre-release letter" == 1 + + : prerelease + : + $* <'1.2.3-a.b' 2>'invalid pre-release' == 1 + + : final-prerelease + : + $* <'1.2.3-b.0' 2>'invalid final pre-release' == 1 + + : snapshot-num + : + $* <'1.2.3-a.1.0' 2>'invalid snapshot number' == 1 + + : snapshot-id + : + $* <'1.2.3-a.1.1.@' 2>'invalid snapshot id' == 1 + + : revision + : + { + : non-prerelease + : + $* <'1.2.3+a' 2>'invalid revision' == 1 + + : prerelease + : + $* <'1.2.3-a.1+a' 2>'invalid revision' == 1 + + : snapshot-num + : + $* <'1.2.3-a.0.1+a' 2>'invalid revision' == 1 + + : snapshot-id + : + $* <'1.2.3-a.0.1.83jdgsf+0' 2>'invalid revision' == 1 + } + + : trailing-junk-after + : + { + : snapshot-num + : + $* <'1.2.3-a.1.z.a' 2>'junk after version' == 1 + + : revision + : + $* <'1.2.3-a.1.z+1a' 2>'junk after version' == 1 + } +} + +: alpha +: +{ + test.options += -a + + $* '1.2.3' >n: non-prerelease + $* '1.2.3-b.1' >n: beta + $* '1.2.3-a.1' >y: final + $* '1.2.3-a.0.1' >y: snapshot +} + +: beta +: +{ + test.options += -b + + $* '1.2.3' >n: non-prerelease + $* '1.2.3-a.1' >n: alpha + $* '1.2.3-b.1' >y: final + $* '1.2.3-b.0.1' >y: snapshot +} + +: compare +: +{ + test.options += -c + + : epoch + : + { + $* '4~1.2.3' '4~1.2.3' >'0' : equal + $* '1.2.4' '4~1.2.3' >'-1': less + } + + : non-prerelease + : + { + $* '1.2.3' '1.2.3' >'0' : equal + $* '1.2.3' '1.2.4' >'-1' : less + } + + : prerelease + : + { + $* '1.2.3-a.1' '1.2.3-a.1' >'0' : equal + $* '1.2.3' '1.2.3-a.1' >'1' : release-gt-prereleas + $* '1.2.3-a.2' '1.2.3-b.1' >'-1' : a-lt-b + $* '1.2.3-a.1' '1.2.3-a.1.2' >'-1' : final-lt-snapshot + $* '1.2.3-a.1.2.xy' '1.2.3-a.1.2' >'0' : ignore-snapshot-id + } +} -- cgit v1.1