From ccad54fefd2d8c980c61ae487dd3bcc9237a6137 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 24 Jun 2015 16:57:46 +0200 Subject: Implement package version struct --- bpkg/manifest | 90 ++++++++++++++++- bpkg/manifest.cxx | 212 +++++++++++++++++++++++++++++++++------ tests/buildfile | 4 +- tests/manifest/driver.cxx | 2 +- tests/manifest/manifest | 4 +- tests/package-version/buildfile | 9 ++ tests/package-version/driver.cxx | 181 +++++++++++++++++++++++++++++++++ 7 files changed, 466 insertions(+), 36 deletions(-) create mode 100644 tests/package-version/buildfile create mode 100644 tests/package-version/driver.cxx diff --git a/bpkg/manifest b/bpkg/manifest index a46dcab..6f3dc13 100644 --- a/bpkg/manifest +++ b/bpkg/manifest @@ -7,6 +7,7 @@ #include #include +#include // uint16 #include // move() #include @@ -19,6 +20,90 @@ namespace bpkg using strings = std::vector; + struct version + { + // Create a special empty version. + // + version (): epoch_ (0), revision_ (0) {} + + explicit + version (const char*); + + explicit + version (const std::string& v): version (v.c_str ()) /* Delegate */ {} + + std::uint16_t + epoch () const noexcept {return epoch_;} + + std::uint16_t + revision () const noexcept {return revision_;} + + const std::string& + upstream () const noexcept {return upstream_;} + + const std::string& + canonical_upstream () const noexcept {return canonical_;} + + std::string + string () const + { + const std::string& v ( + epoch_ != 0 ? std::to_string (epoch_) + "+" + upstream_ : upstream_); + + return revision_ != 0 ? v + "-" + std::to_string (revision_) : v; + } + + bool + operator< (const version& v) const noexcept {return compare (v) < 0;} + + bool + operator> (const version& v) const noexcept {return compare (v) > 0;} + + bool + operator== (const version& v) const noexcept {return compare (v) == 0;} + + bool + operator<= (const version& v) const noexcept {return compare (v) <= 0;} + + bool + operator>= (const version& v) const noexcept {return compare (v) >= 0;} + + bool + operator!= (const version& v) const noexcept {return compare (v) != 0;} + + int + compare (const version& v, bool ignore_revision = false) const noexcept + { + if (epoch_ != v.epoch_) + return epoch_ < v.epoch_ ? -1 : 1; + + if (int c = canonical_.compare (v.canonical_)) + return c; + + if (!ignore_revision && revision_ != v.revision_) + return revision_ < v.revision_ ? -1 : 1; + + return 0; + } + + bool + empty () const noexcept + { + // No sense to test epoch_ and revision_ for 0 as properly constructed + // version object can not have them different from 0 if upstream_ is + // empty. Returns true only for objects constructed with the default + // constructor. + // + return upstream_.empty (); + } + + private: + std::uint16_t epoch_; + std::uint16_t revision_; + std::string upstream_; + std::string canonical_; // Upstream part canonical representation. + }; + // priority // class priority @@ -114,7 +199,7 @@ namespace bpkg struct version_comparison { - std::string value; + version value; comparison operation; }; @@ -149,13 +234,14 @@ namespace bpkg class package_manifest { public: + using version_type = bpkg::version; using priority_type = bpkg::priority; using url_type = bpkg::url; using email_type = bpkg::email; using description_type = bpkg::description; std::string name; - std::string version; + version_type version; butl::optional priority; std::string summary; std::vector license_alternatives; diff --git a/bpkg/manifest.cxx b/bpkg/manifest.cxx index 6a09d53..149c743 100644 --- a/bpkg/manifest.cxx +++ b/bpkg/manifest.cxx @@ -4,12 +4,14 @@ #include +#include #include #include #include #include // strncmp() #include // move() #include // find() +#include // invalid_argument #include #include @@ -35,6 +37,18 @@ namespace bpkg return c == ' ' || c == '\t'; } + inline static bool + digit (char c) + { + return c >= '0' && c <= '9'; + } + + inline static bool + alpha (char c) + { + return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; + } + static ostream& operator<< (ostream& o, const dependency& d) { @@ -45,7 +59,7 @@ namespace bpkg static const char* operations[] = {"==", "<", ">", "<=", ">="}; o << " " << operations[static_cast (d.version->operation)] - << " " << d.version->value; + << " " << d.version->value.string (); } return o; @@ -53,7 +67,7 @@ namespace bpkg // Resize v up to ';', return what goes after ';'. // - static string + inline static string add_comment (const string& v, const string& c) { return c.empty () ? v : (v + "; " + c); @@ -66,7 +80,7 @@ namespace bpkg iterator b (v.begin ()); iterator i (b); - iterator ve (b); // End of value + iterator ve (b); // End of value. iterator e (v.end ()); // Find end of value (ve). @@ -84,12 +98,12 @@ namespace bpkg for (++i; i != e && space (*i); ++i); } - string c(i, e); + string c (i, e); v.resize (ve - b); return c; } - template + template static string concatenate (const T& s, const char* delim = ", ") { @@ -142,8 +156,10 @@ namespace bpkg iterator e (pos_); // End of list item. for (char c; i != end_ && (c = *i) != delim_; ++i) + { if (!space (c)) e = i + 1; + } if (e - pos_ > 0) r.assign (pos_, e); @@ -154,6 +170,141 @@ namespace bpkg return r; } + // version + // + version:: + version (const char* v): version () // Delegate + { + using std::string; // Otherwise compiler get confused with string() member. + + assert (v != nullptr); + + auto bad_arg ([](const string& d) {throw invalid_argument (d);}); + + auto uint16 ( + [&bad_arg](const string& s, const char* what) -> uint16_t + { + unsigned long long v (stoull (s)); + + if (v > UINT16_MAX) // From . + bad_arg (string (what) + " should be 2-byte unsigned integer"); + + return static_cast (v); + }); + + auto add_canonical_component ( + [this, &bad_arg](const char* b, const char* e, bool numeric) -> bool + { + if (!canonical_.empty ()) + canonical_.append (1, '.'); + + if (numeric) + { + if (e - b > 8) + bad_arg ("8 digits maximum allowed in a component"); + + canonical_.append (8 - (e - b), '0'); // Add padding spaces. + + string c (b, e); + canonical_.append (c); + return stoul (c) != 0; + } + else + { + // Don't use tolower to keep things locale independent. + // + static unsigned char shift ('a' - 'A'); + + for (const char* i (b); i != e; ++i) + { + char c (*i); + canonical_.append (1, c >= 'A' && c <='Z' ? c + shift : c); + } + + return true; + } + }); + + enum {epoch, upstream, revision} mode (epoch); + + const char* cb (v); // Begin of a component. + const char* ub (v); // Begin of upstream component. + const char* ue (v); // End of upstream component. + const char* lnn (v - 1); // Last non numeric char. + + // Length of upstream version canonical representation without trailing + // digit-only zero components. + // + size_t cl (0); + + const char* p (v); + for (char c; (c = *p) != '\0'; ++p) + { + switch (c) + { + case '+': + { + if (mode != epoch || p == v) + bad_arg ("unexpected '+' character position"); + + if (lnn >= cb) // Contains non-digits. + bad_arg ("epoch should be 2-byte unsigned integer"); + + epoch_ = uint16 (string (cb, p), "epoch"); + mode = upstream; + cb = p + 1; + ub = cb; + break; + } + + case '-': + case '.': + { + if (mode != epoch && mode != upstream || p == cb) + bad_arg (string ("unexpected '") + c + "' character position"); + + if (add_canonical_component (cb, p, lnn < cb)) + cl = canonical_.size (); + + ue = p; + mode = c == '-' ? revision : upstream; + cb = p + 1; + break; + } + default: + { + if (!digit (c) && !alpha (c)) + bad_arg ("alpha-numeric characters expected in a component"); + } + } + + if (!digit (c)) + lnn = p; + } + + if (p == cb) + bad_arg ("unexpected end"); + + if (mode == revision) + { + if (lnn >= cb) // Contains non-digits. + bad_arg ("revision should be 2-byte unsigned integer"); + + revision_ = uint16 (cb, "revision"); + } + else + { + if (add_canonical_component (cb, p, lnn < cb)) + cl = canonical_.size (); + + ue = p; + } + + assert (ub != ue); // Can't happen if through all previous checks. + upstream_.assign (ub, ue); + canonical_.resize (cl); + } + // package_manifest // package_manifest:: @@ -205,14 +356,14 @@ namespace bpkg if (!version.empty ()) bad_name ("package version redefinition"); - // Think about using class other then string which does value - // verification, proper collation and maybe provides normalized - // representation. - // - if (v.empty ()) - bad_value ("empty package version"); - - version = move (v); + try + { + version = version_type (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid package version: ") + e.what ()); + } } else if (n == "summary") { @@ -362,9 +513,7 @@ namespace bpkg list_parser lp (v.begin (), v.end ()); for (string lv (lp.next ()); !lv.empty (); lv = lp.next ()) - { l.push_back (move (lv)); - } if (l.empty ()) bad_value ("empty package license specification"); @@ -386,9 +535,7 @@ namespace bpkg list_parser lp (b, e, '|'); for (string lv (lp.next ()); !lv.empty (); lv = lp.next ()) - { ra.push_back (lv); - } if (ra.empty () && ra.comment.empty ()) bad_value ("empty package requirement specification"); @@ -415,21 +562,26 @@ namespace bpkg iterator b (lv.begin ()); iterator i (b); - iterator ne (b); // End of name + iterator ne (b); // End of name. iterator e (lv.end ()); // Find end of name (ne). // for (char c; i != e && (c = *i) != '=' && c != '<' && c != '>'; ++i) + { if (!space (c)) ne = i + 1; + } if (i == e) - { da.push_back (dependency {lv}); - } else { + string nm (b, ne); + + if (nm.empty ()) + bad_value ("prerequisite package name not specified"); + // Got to version comparison. // const char* op (&*i); @@ -468,19 +620,19 @@ namespace bpkg if (pos == string::npos) bad_value ("no prerequisite package version specified"); - // Still need to implement version verification. - // - dependency d - { - string (b, ne), - version_comparison {lv.c_str () + pos, operation} - }; + version_type v; - if (d.name.empty ()) + try { - bad_value ("prerequisite package name not specified"); + v = version_type (lv.c_str () + pos); + } + catch (const invalid_argument& e) + { + bad_value ( + string ("invalid prerequisite package version: ") + e.what ()); } + dependency d{move (nm), version_comparison {move (v), operation}}; da.push_back (move (d)); } } @@ -515,7 +667,7 @@ namespace bpkg { s.next ("", "1"); // Start of manifest. s.next ("name", name); - s.next ("version", version); + s.next ("version", version.string ()); if (priority) { diff --git a/tests/buildfile b/tests/buildfile index 1f70dd5..7162cc7 100644 --- a/tests/buildfile +++ b/tests/buildfile @@ -2,6 +2,8 @@ # copyright : Copyright (c) 2014-2015 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -d = manifest-parser/ manifest-serializer/ manifest-roundtrip/ manifest/ +d = manifest-parser/ manifest-serializer/ manifest-roundtrip/ manifest/ \ + package-version/ + .: $d include $d diff --git a/tests/manifest/driver.cxx b/tests/manifest/driver.cxx index ff6005c..f3b5abf 100644 --- a/tests/manifest/driver.cxx +++ b/tests/manifest/driver.cxx @@ -38,7 +38,7 @@ main (int argc, char* argv[]) cerr << "io failure" << endl; return 1; } - catch (const std::exception& e) + catch (const exception& e) { cerr << e.what () << endl; return 1; diff --git a/tests/manifest/manifest b/tests/manifest/manifest index 8d4da6c..6aac49e 100644 --- a/tests/manifest/manifest +++ b/tests/manifest/manifest @@ -31,7 +31,7 @@ requires: zlib; Most Linux/UNIX systems already have one; or get it at\ www.zlib.net. : name: libbar -version: 3.4A.5-a6 +version: 3.4A.5-6 summary: Modern bar management framework license: LGPLv2 tags: c++, xml, modern @@ -40,7 +40,7 @@ url: http://www.example.org/projects/libbar/ email: libbar-users@example.org : name: libbaz -version: 3.4A.5-a6 +version: 2+3.4A.5-3 summary: Modern baz system license: LGPLv2 url: http://www.example.org/projects/libbar/ diff --git a/tests/package-version/buildfile b/tests/package-version/buildfile new file mode 100644 index 0000000..38430dc --- /dev/null +++ b/tests/package-version/buildfile @@ -0,0 +1,9 @@ +# file : tests/package-version/buildfile +# copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +exe{driver}: cxx{driver} ../../bpkg/lib{bpkg} + +include ../../bpkg/ + +# test: ./driver diff --git a/tests/package-version/driver.cxx b/tests/package-version/driver.cxx new file mode 100644 index 0000000..5508c20 --- /dev/null +++ b/tests/package-version/driver.cxx @@ -0,0 +1,181 @@ +// file : tests/package-version/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include +#include +#include +#include +#include // invalid_argument + +#include + +using namespace std; +using namespace bpkg; + +static bool +bad_version (const string& v) +{ + try + { + version bv (v); + return false; + } + catch (const invalid_argument&) + { + return true; + } +} + +int +main (int argc, char* argv[]) +{ + if (argc != 1) + { + cerr << "usage: " << argv[0] << endl; + return 1; + } + + try + { + assert (bad_version ("")); // Empty upstream. + assert (bad_version ("1+")); // Same. + assert (bad_version ("1+-3")); // Same. + assert (bad_version ("-3")); // Same. + assert (bad_version ("+3.5")); // Empty epoch. + assert (bad_version ("a-")); // Empty revision. + assert (bad_version ("1+2+4.1-3")); // Extra epoch. + assert (bad_version ("3.5-1-4")); // Extra revision. + assert (bad_version ("1++2-3")); // Duplicated epoch separator. + assert (bad_version ("1+2--3")); // Duplicated revision separator. + assert (bad_version ("a.394857391.3")); // Too long numeric component. + assert (bad_version ("a.000000000.3")); // Too long numeric zero component. + assert (bad_version ("65536+q.3")); // Too big epoch. + assert (bad_version ("1+q-65536")); // Too big revision. + assert (bad_version ("3.5+1.4")); // Components in epoch. + assert (bad_version ("3.5-1.4")); // Components in revision. + assert (bad_version ("3 5-1")); // Non alpha-numeric in upstream. + assert (bad_version ("1+ -3")); // Same. + assert (bad_version ("3 5+4-1")); // Non alpha-numeric in epoch. + assert (bad_version ("2b+a")); // Same. + assert (bad_version ("1+34.1-3 5")); // Non alpha-numeric in revision. + assert (bad_version ("a-3s")); // Same. + assert (bad_version ("a.")); // Not completed upstream. + assert (bad_version ("a..b")); // Empty upstream component. + + { + version v ("a"); + assert (v.string () == "a"); + assert (v.canonical_upstream () == "a"); + } + + { + version v ("65535+ab-65535"); + assert (v.string () == "65535+ab-65535"); + assert (v.canonical_upstream () == "ab"); + } + + { + version v ("1"); + assert (v.string () == "1"); + assert (v.canonical_upstream () == "00000001"); + } + + { + version v ("0"); + assert (v.string () == "0"); + assert (v.canonical_upstream ().empty ()); + } + + { + version v ("0.0.0"); + assert (v.string () == "0.0.0"); + assert (v.canonical_upstream ().empty ()); + } + + { + version v ("1.0.0"); + assert (v.string () == "1.0.0"); + assert (v.canonical_upstream () == "00000001"); + } + + { + version v ("0.1.00"); + assert (v.string () == "0.1.00"); + assert (v.canonical_upstream () == "00000000.00000001"); + } + + { + version v ("0.0a.00"); + assert (v.string () == "0.0a.00"); + assert (v.canonical_upstream () == "00000000.0a"); + } + + { + version v ("0.a00.00"); + assert (v.string () == "0.a00.00"); + assert (v.canonical_upstream () == "00000000.a00"); + } + + { + version v ("1+0"); + assert (v.string () == "1+0"); + assert (v.canonical_upstream ().empty ()); + } + + { + version v ("0+A-1"); + assert (v.string () == "A-1"); + assert (v.canonical_upstream () == "a"); + } + + { + version v ("10+B-0"); + assert (v.string () == "10+B"); + assert (v.canonical_upstream () == "b"); + } + + { + version v ("3+1A.31.0.4.0-7"); + assert (v.string () == "3+1A.31.0.4.0-7"); + assert (v.canonical_upstream () == "1a.00000031.00000000.00000004"); + } + + assert (version ("a") == version ("a")); + assert (version ("a") < version ("b")); + assert (version ("a") < version ("aa")); + assert (version ("a.a") < version ("aaa")); + assert (version ("a") < version ("a.a")); + assert (version ("ab") == version ("ab")); + assert (version ("ac") < version ("bc")); + assert (version ("ab-0") == version ("ab")); + assert (version ("a.1-1") > version ("a.1")); + assert (version ("0+ab") == version ("ab")); + assert (version ("1.2") > version ("1.1")); + assert (version ("1+1.0") > version ("2.0")); + assert (version ("0+ab-1") == version ("ab-1")); + assert (version ("0+ab-1").compare (version ("0+ab-2"), true) == 0); + assert (version ("12") > version ("2")); + assert (version ("2") < version ("12")); + assert (version ("1") == version ("01")); + assert (version ("1") == version ("1.0")); + assert (version ("1.3") == version ("1.3.0")); + assert (version ("1.3") == version ("1.3.0.0")); + assert (version ("1.3.1") > version ("1.3")); + assert (version ("1.30") > version ("1.5")); + assert (version ("1.alpha.1") < version ("1.Beta.1")); + assert (version ("1.Alpha.1") < version ("1.beta.1")); + assert (version ("1.Alpha.1") == version ("1.ALPHA.1")); + assert (version ("a.1") < version ("ab1")); + assert (version ("a.2") < version ("a.1b")); + assert (version ("0.0.0") == version ("0")); + assert (version ("1.0.0") == version ("01")); + assert (version ("0.1.00") == version ("00.1")); + assert (version ("0.0a.00") == version ("00.0a")); + } + catch (const exception& e) + { + cerr << e.what () << endl; + return 1; + } +} -- cgit v1.1