aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2015-06-18 20:47:50 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2015-06-22 15:32:14 +0200
commit5a5be442f4604b4634ed55a4d3980addcf60838f (patch)
treeb6bcf6f90e8f3cc1beeb8979253534d68d5ab920
parent165ebf5e12d5c421df06d4371295c7a10e30beba (diff)
Package manifest parsing and serialization
Package version is not verified yet. Still need to think about using class other then string which does value verification, proper collation and maybe provides normalized representation.
-rw-r--r--bpkg/manifest58
-rw-r--r--bpkg/manifest.cxx550
-rw-r--r--tests/manifest/manifest40
3 files changed, 617 insertions, 31 deletions
diff --git a/bpkg/manifest b/bpkg/manifest
index c920cd6..a46dcab 100644
--- a/bpkg/manifest
+++ b/bpkg/manifest
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
+#include <algorithm> // move()
#include <butl/optional>
@@ -25,13 +26,13 @@ namespace bpkg
public:
enum value_type {low, medium, high, security};
+ value_type value; // Shouldn't be necessary to access directly.
std::string comment;
- priority (value_type v = low): value (v) {}
- operator value_type () const {return value;}
+ priority (value_type v = low, std::string c = "")
+ : value (v), comment (std::move (c)) {}
- private:
- value_type value; // Shouldn't be necessary to access directly.
+ operator value_type () const {return value;}
};
// description
@@ -41,6 +42,17 @@ namespace bpkg
{
bool file;
std::string comment;
+
+ // Description constructor.
+ //
+ explicit
+ description (std::string d = "")
+ : std::string (std::move (d)), file (false) {}
+
+ // Description file constructor.
+ //
+ description (std::string f, std::string c)
+ : std::string (std::move (f)), file (true), comment (std::move (c)) {}
};
// license
@@ -48,6 +60,9 @@ namespace bpkg
struct licenses: strings
{
std::string comment;
+
+ explicit
+ licenses (std::string c = ""): comment (std::move (c)) {}
};
// change
@@ -57,6 +72,16 @@ namespace bpkg
{
bool file;
std::string comment;
+
+ // Change constructor.
+ //
+ explicit
+ change (std::string c = ""): std::string (std::move (c)), file (false) {}
+
+ // Change file constructor.
+ //
+ change (std::string f, std::string c)
+ : std::string (std::move (f)), file (true), comment (std::move (c)) {}
};
// url
@@ -65,6 +90,10 @@ namespace bpkg
struct url: std::string
{
std::string comment;
+
+ explicit
+ url (std::string u = "", std::string c = "")
+ : std::string (std::move (u)), comment (std::move (c)) {}
};
// email
@@ -73,6 +102,10 @@ namespace bpkg
struct email: std::string
{
std::string comment;
+
+ explicit
+ email (std::string e = "", std::string c = "")
+ : std::string (std::move (e)), comment (std::move (c)) {}
};
// depends
@@ -95,6 +128,10 @@ namespace bpkg
{
bool conditional;
std::string comment;
+
+ explicit
+ dependency_alternatives (bool d = false, std::string c = "")
+ : conditional (d), comment (std::move (c)) {}
};
// requires
@@ -103,6 +140,10 @@ namespace bpkg
{
bool conditional;
std::string comment;
+
+ explicit
+ requirement_alternatives (bool d = false, std::string c = "")
+ : conditional (d), comment (std::move (c)) {}
};
class package_manifest
@@ -111,14 +152,15 @@ namespace bpkg
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;
- priority_type priority;
+ butl::optional<priority_type> priority;
std::string summary;
std::vector<licenses> license_alternatives;
strings tags;
- std::vector<description> descriptions;
+ butl::optional<description_type> description;
std::vector<change> changes;
url_type url;
butl::optional<url_type> package_url;
@@ -129,7 +171,7 @@ namespace bpkg
public:
package_manifest (manifest_parser&);
- package_manifest (manifest_parser&, const manifest_name_value& start);
+ package_manifest (manifest_parser&, manifest_name_value start);
void
serialize (manifest_serializer&) const;
@@ -142,7 +184,7 @@ namespace bpkg
public:
repository_manifest (manifest_parser&);
- repository_manifest (manifest_parser&, const manifest_name_value& start);
+ repository_manifest (manifest_parser&, manifest_name_value start);
void
serialize (manifest_serializer&) const;
diff --git a/bpkg/manifest.cxx b/bpkg/manifest.cxx
index 85e4296..6a09d53 100644
--- a/bpkg/manifest.cxx
+++ b/bpkg/manifest.cxx
@@ -4,7 +4,12 @@
#include <bpkg/manifest>
-#include <utility> // move()
+#include <ostream>
+#include <sstream>
+#include <cassert>
+#include <cstring> // strncmp()
+#include <utility> // move()
+#include <algorithm> // find()
#include <bpkg/manifest-parser>
#include <bpkg/manifest-serializer>
@@ -19,6 +24,136 @@ namespace bpkg
using serialization = manifest_serialization;
using name_value = manifest_name_value;
+ // Utility functions
+ //
+ static const strings priority_names ({"low", "medium", "high", "security"});
+ static const string spaces (" \t");
+
+ inline static bool
+ space (char c)
+ {
+ return c == ' ' || c == '\t';
+ }
+
+ static ostream&
+ operator<< (ostream& o, const dependency& d)
+ {
+ o << d.name;
+
+ if (d.version)
+ {
+ static const char* operations[] = {"==", "<", ">", "<=", ">="};
+
+ o << " " << operations[static_cast<size_t> (d.version->operation)]
+ << " " << d.version->value;
+ }
+
+ return o;
+ }
+
+ // Resize v up to ';', return what goes after ';'.
+ //
+ static string
+ add_comment (const string& v, const string& c)
+ {
+ return c.empty () ? v : (v + "; " + c);
+ }
+
+ static string
+ split_comment (string& v)
+ {
+ using iterator = string::const_iterator;
+
+ iterator b (v.begin ());
+ iterator i (b);
+ iterator ve (b); // End of value
+ iterator e (v.end ());
+
+ // Find end of value (ve).
+ //
+ for (char c; i != e && (c = *i) != ';'; ++i)
+ if (!space (c))
+ ve = i + 1;
+
+ // Find beginning of a comment (i).
+ //
+ if (i != e)
+ {
+ // Skip spaces.
+ //
+ for (++i; i != e && space (*i); ++i);
+ }
+
+ string c(i, e);
+ v.resize (ve - b);
+ return c;
+ }
+
+ template<typename T>
+ static string
+ concatenate (const T& s, const char* delim = ", ")
+ {
+ ostringstream o;
+ for (auto b (s.begin ()), i (b), e (s.end ()); i != e; ++i)
+ {
+ if (i != b)
+ o << delim;
+
+ o << *i;
+ }
+
+ return o.str ();
+ }
+
+ // list_parser
+ //
+ class list_parser
+ {
+ public:
+ using iterator = string::const_iterator;
+
+ public:
+ list_parser (iterator b, iterator e, char d = ',')
+ : pos_ (b), end_ (e), delim_ (d) {}
+
+ string
+ next ();
+
+ private:
+ iterator pos_;
+ iterator end_;
+ char delim_;
+ };
+
+ string list_parser::
+ next ()
+ {
+ string r;
+
+ // Continue until get non empty list item.
+ //
+ while (pos_ != end_ && r.empty ())
+ {
+ // Skip spaces.
+ //
+ for (; pos_ != end_ && space (*pos_); ++pos_);
+
+ iterator i (pos_);
+ 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);
+
+ pos_ = i == end_ ? i : i + 1;
+ }
+
+ return r;
+ }
+
// package_manifest
//
package_manifest::
@@ -34,33 +169,345 @@ namespace bpkg
}
package_manifest::
- package_manifest (parser& p, const name_value& s)
+ package_manifest (parser& p, name_value nv)
{
+ auto bad_name ([&p, &nv](const string& d) {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);});
+
+ auto bad_value ([&p, &nv](const string& d) {
+ throw parsing (p.name (), nv.value_line, nv.value_column, d);});
+
// Make sure this is the start and we support the version.
//
- if (!s.name.empty ())
- throw parsing (p.name (), s.name_line, s.name_column,
- "start of package manifest expected");
+ if (!nv.name.empty ())
+ bad_name ("start of package manifest expected");
- if (s.value != "1")
- throw parsing (p.name (), s.value_line, s.value_column,
- "unsupported format version");
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
- for (name_value nv (p.next ()); !nv.empty (); nv = p.next ())
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
{
string& n (nv.name);
string& v (nv.value);
if (n == "name")
+ {
+ if (!name.empty ())
+ bad_name ("package name redefinition");
+
+ if (v.empty ())
+ bad_value ("empty package name");
+
name = move (v);
- // ...
+ }
+ else if (n == "version")
+ {
+ 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);
+ }
+ else if (n == "summary")
+ {
+ if (!summary.empty ())
+ bad_name ("package summary redefinition");
+
+ if (v.empty ())
+ bad_value ("empty package summary");
+
+ summary = move (v);
+ }
+ else if (n == "tags")
+ {
+ if (!tags.empty ())
+ bad_name ("package tags redefinition");
+
+ list_parser lp (v.begin (), v.end ());
+ for (string lv (lp.next ()); !lv.empty (); lv = lp.next ())
+ {
+ if (lv.find_first_of (spaces) != string::npos)
+ bad_value ("only single-word tags allowed");
+
+ tags.push_back (move (lv));
+ }
+
+ if (tags.empty ())
+ bad_value ("empty package tags specification");
+ }
+ else if (n == "description")
+ {
+ if (description)
+ {
+ if (description->file)
+ bad_name ("package description and description-file are "
+ "mutually exclusive");
+ else
+ bad_name ("package description redefinition");
+ }
+
+ if (v.empty ())
+ bad_value ("empty package description");
+
+ description = description_type (move (v));
+ }
+ else if (n == "description-file")
+ {
+ if (description)
+ {
+ if (description->file)
+ bad_name ("package description-file redefinition");
+ else
+ bad_name ("package description-file and description are "
+ "mutually exclusive");
+ }
+
+ string c (split_comment (v));
+
+ if (v.empty ())
+ bad_value ("no path in package description-file");
+
+ description = description_type (move (v), move (c));
+ }
+ else if (n == "changes")
+ {
+ if (v.empty ())
+ bad_value ("empty package changes specification");
+
+ changes.emplace_back (move (v));
+ }
+ else if (n == "changes-file")
+ {
+ string c (split_comment (v));
+
+ if (v.empty ())
+ bad_value ("no path in package changes-file");
+
+ changes.emplace_back (move (v), move (c));
+ }
+ else if (n == "url")
+ {
+ if (!url.empty ())
+ bad_name ("project url redefinition");
+
+ string c (split_comment (v));
+
+ if (v.empty ())
+ bad_value ("empty project url");
+
+ url = url_type (move (v), move (c));
+ }
+ else if (n == "email")
+ {
+ if (!email.empty ())
+ bad_name ("project email redefinition");
+
+ string c (split_comment (v));
+
+ if (v.empty ())
+ bad_value ("empty project email");
+
+ email = email_type (move (v), move (c));
+ }
+ else if (n == "package-url")
+ {
+ if (package_url)
+ bad_name ("package url redefinition");
+
+ string c (split_comment (v));
+
+ if (v.empty ())
+ bad_value ("empty package url");
+
+ package_url = url_type (move (v), move (c));
+ }
+ else if (n == "package-email")
+ {
+ if (package_email)
+ bad_name ("package email redefinition");
+
+ string c (split_comment (v));
+
+ if (v.empty ())
+ bad_value ("empty package email");
+
+ package_email = email_type (move (v), move (c));
+ }
+ else if (n == "priority")
+ {
+ if (priority)
+ bad_name ("package priority redefinition");
+
+ string c (split_comment (v));
+ strings::const_iterator b (priority_names.begin ());
+ strings::const_iterator e (priority_names.end ());
+ strings::const_iterator i (find (b, e, v));
+
+ if (i == e)
+ bad_value ("invalid package priority");
+
+ priority =
+ priority_type (static_cast<priority_type::value_type> (i - b),
+ move (c));
+ }
+ else if (n == "license")
+ {
+ licenses l (split_comment (v));
+
+ 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");
+
+ license_alternatives.push_back (move (l));
+ }
+ else if (n == "requires")
+ {
+ bool cond (!v.empty () && v[0] == '?');
+ requirement_alternatives ra (cond, split_comment (v));
+ string::const_iterator b (v.begin ());
+ string::const_iterator e (v.end ());
+
+ if (ra.conditional)
+ {
+ string::size_type p (v.find_first_not_of (spaces, 1));
+ b = p == string::npos ? e : b + p;
+ }
+
+ 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");
+
+ requirements.push_back (move (ra));
+ }
+ else if (n == "depends")
+ {
+ bool cond (!v.empty () && v[0] == '?');
+ dependency_alternatives da (cond, split_comment (v));
+ string::const_iterator b (v.begin ());
+ string::const_iterator e (v.end ());
+
+ if (da.conditional)
+ {
+ string::size_type p (v.find_first_not_of (spaces, 1));
+ b = p == string::npos ? e : b + p;
+ }
+
+ list_parser lp (b, e, '|');
+ for (string lv (lp.next ()); !lv.empty (); lv = lp.next ())
+ {
+ using iterator = string::const_iterator;
+
+ iterator b (lv.begin ());
+ iterator i (b);
+ 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
+ {
+ // Got to version comparison.
+ //
+ const char* op (&*i);
+ comparison operation;
+
+ if (strncmp (op, "==", 2) == 0)
+ {
+ operation = comparison::eq;
+ i += 2;
+ }
+ else if (strncmp (op, ">=", 2) == 0)
+ {
+ operation = comparison::ge;
+ i += 2;
+ }
+ else if (strncmp (op, "<=", 2) == 0)
+ {
+ operation = comparison::le;
+ i += 2;
+ }
+ else if (*op == '>')
+ {
+ operation = comparison::gt;
+ ++i;
+ }
+ else if (*op == '<')
+ {
+ operation = comparison::lt;
+ ++i;
+ }
+ else
+ bad_value ("invalid prerequisite package version comparison");
+
+ string::size_type pos = lv.find_first_not_of (spaces, i - b);
+
+ 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}
+ };
+
+ if (d.name.empty ())
+ {
+ bad_value ("prerequisite package name not specified");
+ }
+
+ da.push_back (move (d));
+ }
+ }
+
+ if (da.empty ())
+ bad_value ("empty package dependency specification");
+
+ dependencies.push_back (da);
+ }
else
- throw parsing (p.name (), nv.value_line, nv.value_column,
- "unknown name '" + n + "' in package manifest");
+ bad_name ("unknown name '" + n + "' in package manifest");
}
// Verify all non-optional values were specified.
//
+ if (name.empty ())
+ bad_value ("no package name specified");
+ else if (version.empty ())
+ bad_value ("no package version specified");
+ else if (summary.empty ())
+ bad_value ("no package summary specified");
+ else if (url.empty ())
+ bad_value ("no project url specified");
+ else if (email.empty ())
+ bad_value ("no project email specified");
+ else if (license_alternatives.empty ())
+ bad_value ("no project license specified");
}
void package_manifest::
@@ -68,7 +515,61 @@ namespace bpkg
{
s.next ("", "1"); // Start of manifest.
s.next ("name", name);
- // ...
+ s.next ("version", version);
+
+ if (priority)
+ {
+ priority::value_type v (*priority);
+ assert (v < priority_names.size ());
+ s.next ("priority", add_comment (priority_names[v], priority->comment));
+ }
+
+ s.next ("summary", summary);
+
+ for (const auto& la: license_alternatives)
+ s.next ("license", add_comment (concatenate (la), la.comment));
+
+ if (!tags.empty ())
+ s.next ("tags", concatenate (tags));
+
+ if (description)
+ {
+ if (description->file)
+ s.next ("description-file",
+ add_comment (*description, description->comment));
+ else
+ s.next ("description", *description);
+ }
+
+ for (const auto& c: changes)
+ {
+ if (c.file)
+ s.next ("changes-file", add_comment (c, c.comment));
+ else
+ s.next ("changes", c);
+ }
+
+ s.next ("url", add_comment (url, url.comment));
+
+ if (package_url)
+ s.next ("package-url", add_comment (*package_url, package_url->comment));
+
+ s.next ("email", add_comment (email, email.comment));
+
+ if (package_email)
+ s.next ("package-email",
+ add_comment (*package_email, package_email->comment));
+
+ for (const auto& d: dependencies)
+ s.next ("depends",
+ (d.conditional ? "? " : "") +
+ add_comment (concatenate (d, " | "), d.comment));
+
+ for (const auto& r: requirements)
+ s.next ("requires",
+ (r.conditional ? "? " : "") +
+ add_comment (concatenate (r, " | "), r.comment));
+
s.next ("", ""); // End of manifest.
}
@@ -87,19 +588,23 @@ namespace bpkg
}
repository_manifest::
- repository_manifest (parser& p, const name_value& s)
+ repository_manifest (parser& p, name_value nv)
{
+ auto bad_name ([&p, &nv](const string& d) {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);});
+
+ auto bad_value ([&p, &nv](const string& d) {
+ throw parsing (p.name (), nv.value_line, nv.value_column, d);});
+
// Make sure this is the start and we support the version.
//
- if (!s.name.empty ())
- throw parsing (p.name (), s.name_line, s.name_column,
- "start of repository manifest expected");
+ if (!nv.name.empty ())
+ bad_name ("start of repository manifest expected");
- if (s.value != "1")
- throw parsing (p.name (), s.value_line, s.value_column,
- "unsupported format version");
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
- for (name_value nv (p.next ()); !nv.empty (); nv = p.next ())
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
{
string& n (nv.name);
string& v (nv.value);
@@ -107,8 +612,7 @@ namespace bpkg
if (n == "location")
location = move (v);
else
- throw parsing (p.name (), nv.value_line, nv.value_column,
- "unknown name '" + n + "' in repository manifest");
+ bad_name ("unknown name '" + n + "' in repository manifest");
}
// Verify all non-optional values were specified.
diff --git a/tests/manifest/manifest b/tests/manifest/manifest
index a295dd7..8d4da6c 100644
--- a/tests/manifest/manifest
+++ b/tests/manifest/manifest
@@ -3,5 +3,45 @@ location: http://pkg.example.org/1/math
:
:
name: libfoo
+version: 1.2.3-2
+priority: high; Due to critical bug fix.
+summary: Modern XML parser
+license: LGPLv2, MIT; Both required.
+license: BSD
+tags: c++, xml, parser, serializer, pull, streaming, modern
+description: libfoo is a very modern C++ XML parser.
+changes: 1.2.3-2: applied upstream patch for critical bug bar
+changes: 1.2.3-1: applied upstream patch for critical bug foo
+changes-file: NEWS
+url: http://www.example.org/projects/libfoo/; libfoo project page url
+package-url: http://www.example.org/projects/libbar/1.2.3-2; package url
+email: libfoo-users@example.org; Public mailing list, posts by non-members\
+ are allowed but moderated.
+package-email: libfoo-1.2.3-2@example.org; Bug reports are welcome.
+depends: libz
+depends: libgnutls <= 1.2.3 | libopenssl >= 2.3.4
+depends: ? libboost-regex >= 1.52.0; Only if C++ compiler doesn't support\
+ C++11 <regex>.
+depends: ? libqtcore >= 5.0.0; Only if GUI is enabled.
+requires: linux | windows | macosx; symbian is coming.
+requires: c++11
+requires: ? ; VC++ 12.0 or later if targeting Windows.
+requires: ? ; libc++ standard library if using Clang on Mac OS X.
+requires: zlib; Most Linux/UNIX systems already have one; or get it at\
+ www.zlib.net.
:
name: libbar
+version: 3.4A.5-a6
+summary: Modern bar management framework
+license: LGPLv2
+tags: c++, xml, modern
+description-file: README; Comprehensive description
+url: http://www.example.org/projects/libbar/
+email: libbar-users@example.org
+:
+name: libbaz
+version: 3.4A.5-a6
+summary: Modern baz system
+license: LGPLv2
+url: http://www.example.org/projects/libbar/
+email: libbaz-users@example.org