aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2015-06-24 16:57:46 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2015-06-26 14:33:11 +0200
commitccad54fefd2d8c980c61ae487dd3bcc9237a6137 (patch)
tree36566cb8e072c444cf8e344a5844800f7e177c86
parent5a5be442f4604b4634ed55a4d3980addcf60838f (diff)
Implement package version struct
-rw-r--r--bpkg/manifest90
-rw-r--r--bpkg/manifest.cxx212
-rw-r--r--tests/buildfile4
-rw-r--r--tests/manifest/driver.cxx2
-rw-r--r--tests/manifest/manifest4
-rw-r--r--tests/package-version/buildfile9
-rw-r--r--tests/package-version/driver.cxx181
7 files changed, 466 insertions, 36 deletions
diff --git a/bpkg/manifest b/bpkg/manifest
index a46dcab..6f3dc13 100644
--- a/bpkg/manifest
+++ b/bpkg/manifest
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
+#include <cstdint> // uint16
#include <algorithm> // move()
#include <butl/optional>
@@ -19,6 +20,90 @@ namespace bpkg
using strings = std::vector<std::string>;
+ 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_type> priority;
std::string summary;
std::vector<licenses> 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 <bpkg/manifest>
+#include <string>
#include <ostream>
#include <sstream>
#include <cassert>
#include <cstring> // strncmp()
#include <utility> // move()
#include <algorithm> // find()
+#include <stdexcept> // invalid_argument
#include <bpkg/manifest-parser>
#include <bpkg/manifest-serializer>
@@ -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<size_t> (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<typename T>
+ template <typename T>
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 <cstdint>.
+ bad_arg (string (what) + " should be 2-byte unsigned integer");
+
+ return static_cast<uint16_t> (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 <string>
+#include <cassert>
+#include <iostream>
+#include <exception>
+#include <stdexcept> // invalid_argument
+
+#include <bpkg/manifest>
+
+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;
+ }
+}