From c4d2ac250aee4102b519ce1db89bde3fe7855639 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Mon, 1 May 2017 12:10:35 +0300 Subject: Add hxx extension for headers and lib prefix for library dirs --- libbpkg/.gitignore | 1 + libbpkg/buildfile | 36 + libbpkg/export.hxx | 41 + libbpkg/manifest.cxx | 2190 ++++++++++++++++++++++++++++++++++++++++++++++++ libbpkg/manifest.hxx | 636 ++++++++++++++ libbpkg/version.hxx.in | 44 + 6 files changed, 2948 insertions(+) create mode 100644 libbpkg/.gitignore create mode 100644 libbpkg/buildfile create mode 100644 libbpkg/export.hxx create mode 100644 libbpkg/manifest.cxx create mode 100644 libbpkg/manifest.hxx create mode 100644 libbpkg/version.hxx.in (limited to 'libbpkg') diff --git a/libbpkg/.gitignore b/libbpkg/.gitignore new file mode 100644 index 0000000..426db9e --- /dev/null +++ b/libbpkg/.gitignore @@ -0,0 +1 @@ +version.hxx diff --git a/libbpkg/buildfile b/libbpkg/buildfile new file mode 100644 index 0000000..8fda226 --- /dev/null +++ b/libbpkg/buildfile @@ -0,0 +1,36 @@ +# file : libbpkg/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import int_libs = libbutl%lib{butl} + +lib{bpkg}: \ +{hxx }{ export } \ +{hxx cxx}{ manifest } \ +{hxx }{ version } \ + $int_libs + +hxx{version}: in{version} $src_root/file{manifest} +hxx{version}: dist = true + +# For pre-releases use the complete version to make sure they cannot be used +# in place of another pre-release or the final version. +# +if $version.pre_release + lib{bpkg}: bin.lib.version = @"-$version.project_id" +else + lib{bpkg}: bin.lib.version = @"-$version.major.$version.minor" + +cxx.poptions =+ "-I$out_root" "-I$src_root" +obja{*}: cxx.poptions += -DLIBBPKG_STATIC_BUILD +objs{*}: cxx.poptions += -DLIBBPKG_SHARED_BUILD + +lib{bpkg}: cxx.export.poptions = "-I$out_root" "-I$src_root" +liba{bpkg}: cxx.export.poptions += -DLIBBPKG_STATIC +libs{bpkg}: cxx.export.poptions += -DLIBBPKG_SHARED + +lib{bpkg}: cxx.export.libs = $int_libs + +# Install into the libbpkg/ subdirectory of, say, /usr/include/. +# +install.include = $install.include/libbpkg/ diff --git a/libbpkg/export.hxx b/libbpkg/export.hxx new file mode 100644 index 0000000..39f33e9 --- /dev/null +++ b/libbpkg/export.hxx @@ -0,0 +1,41 @@ +// file : libbpkg/export.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBPKG_EXPORT_HXX +#define LIBBPKG_EXPORT_HXX + +// Normally we don't export class templates (but do complete specializations), +// inline functions, and classes with only inline member functions. Exporting +// classes that inherit from non-exported/imported bases (e.g., std::string) +// will end up badly. The only known workarounds are to not inherit or to not +// export. Also, MinGW GCC doesn't like seeing non-exported function being +// used before their inline definition. The workaround is to reorder code. In +// the end it's all trial and error. + +#if defined(LIBBPKG_STATIC) // Using static. +# define LIBBPKG_EXPORT +#elif defined(LIBBPKG_STATIC_BUILD) // Building static. +# define LIBBPKG_EXPORT +#elif defined(LIBBPKG_SHARED) // Using shared. +# ifdef _WIN32 +# define LIBBPKG_EXPORT __declspec(dllimport) +# else +# define LIBBPKG_EXPORT +# endif +#elif defined(LIBBPKG_SHARED_BUILD) // Building shared. +# ifdef _WIN32 +# define LIBBPKG_EXPORT __declspec(dllexport) +# else +# define LIBBPKG_EXPORT +# endif +#else +// If none of the above macros are defined, then we assume we are being used +// by some third-party build system that cannot/doesn't signal the library +// type. Note that this fallback works for both static and shared but in case +// of shared will be sub-optimal compared to having dllimport. +// +# define LIBBPKG_EXPORT // Using static or shared. +#endif + +#endif // LIBBPKG_EXPORT_HXX diff --git a/libbpkg/manifest.cxx b/libbpkg/manifest.cxx new file mode 100644 index 0000000..2b34695 --- /dev/null +++ b/libbpkg/manifest.cxx @@ -0,0 +1,2190 @@ +// file : libbpkg/manifest.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include +#include // strncmp(), strcmp() +#include // move() +#include // uint64_t, uint16_t, UINT16_MAX +#include // back_insert_iterator +#include // find(), transform() +#include // invalid_argument + +#include +#include +#include // casecmp(), lcase(), alpha(), digit() +#include +#include + +using namespace std; +using namespace butl; + +namespace bpkg +{ + using parser = manifest_parser; + using parsing = manifest_parsing; + using serializer = manifest_serializer; + using serialization = manifest_serialization; + using name_value = manifest_name_value; + + // Utility functions + // + static const strings priority_names ({"low", "medium", "high", "security"}); + static const strings repository_role_names ( + {"base", "prerequisite", "complement"}); + + static const string spaces (" \t"); + + inline static bool + space (char c) noexcept + { + return c == ' ' || c == '\t'; + } + + inline static bool + valid_sha256 (const string& s) noexcept + { + if (s.size () != 64) + return false; + + for (const auto& c: s) + { + if ((c < 'a' || c > 'f' ) && !digit (c)) + return false; + } + + return true; + } + + // Resize v up to ';', return what goes after ';'. + // + inline 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 + 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; + } + + // version + // + version:: + version (uint16_t e, std::string u, optional l, uint16_t r) + : epoch (e), + upstream (move (u)), + release (move (l)), + revision (r), + canonical_upstream ( + data_type (upstream.c_str (), data_type::parse::upstream). + canonical_upstream), + canonical_release ( + data_type (release ? release->c_str () : nullptr, + data_type::parse::release). + canonical_release) + { + // Check members constrains. + // + if (upstream.empty ()) // Constructing empty version. + { + if (epoch != 0) + throw invalid_argument ("epoch for empty version"); + + if (!release || !release->empty ()) + throw invalid_argument ("not-empty release for empty version"); + + if (revision != 0) + throw invalid_argument ("revision for empty version"); + } + else if (release && release->empty () && revision != 0) + // Empty release signifies the earliest possible release. Revision is + // meaningless in such a context. + // + throw invalid_argument ("revision for earliest possible release"); + } + + // Builder of the upstream or release version part canonical representation. + // + struct canonical_part: string + { + string + final () const {return substr (0, len_);} + + void + add (const char* begin, const char* end, bool numeric) + { + if (!empty ()) + append (1, '.'); + + bool zo (false); // Digit-only zero component. + if (numeric) + { + if (end - begin > 16) + throw invalid_argument ("16 digits maximum allowed in a component"); + + append (16 - (end - begin), '0'); // Add padding zeros. + + string c (begin, end); + append (c); + zo = stoul (c) == 0; + } + else + append (lcase (begin, end - begin)); + + if (!zo) + len_ = size (); + } + + private: + size_t len_ = 0; // Length without the trailing digit-only zero components. + }; + + version::data_type:: + data_type (const char* v, parse pr): epoch (0), revision (0) + { + // Otherwise compiler gets confused with string() member. + // + using std::string; + + if (pr == parse::release && v == nullptr) + { + // Special case: final version release part. + // + canonical_release = "~"; + return; + } + + 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); + }); + + enum class mode {epoch, upstream, release, revision}; + mode m (pr == parse::full + ? mode::epoch + : pr == parse::upstream + ? mode::upstream + : mode::release); + + canonical_part canon_upstream; + canonical_part canon_release; + + canonical_part* canon_part ( + pr == parse::release ? &canon_release : &canon_upstream); + + const char* cb (v); // Begin of a component. + const char* ub (v); // Begin of upstream part. + const char* ue (v); // End of upstream part. + const char* rb (v); // Begin of release part. + const char* re (v); // End of release part. + const char* lnn (v - 1); // Last non numeric char. + + const char* p (v); + for (char c; (c = *p) != '\0'; ++p) + { + switch (c) + { + case '~': + { + if (pr != parse::full) + bad_arg ("unexpected '~' character"); + + // Process the epoch part. + // + if (m != 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"); + + m = mode::upstream; + cb = p + 1; + ub = cb; + break; + } + + case '+': + case '-': + case '.': + { + // Process the upsteam or release part component. + // + + // Characters '+', '-' are only valid for the full version parsing. + // + if (c != '.' && pr != parse::full) + bad_arg (string ("unexpected '") + c + "' character"); + + // Check if the component ending is valid for the current parsing + // state. + // + if (m == mode::revision || (c == '-' && m == mode::release) || + p == cb) + bad_arg (string ("unexpected '") + c + "' character position"); + + // Append the component to the current canonical part. + // + canon_part->add (cb, p, lnn < cb); + + // Update the parsing state. + // + cb = p + 1; + + if (m == mode::upstream || m == mode::epoch) + ue = p; + else if (m == mode::release) + re = p; + else + assert (false); + + if (c == '+') + m = mode::revision; + else if (c == '-') + { + m = mode::release; + canon_part = &canon_release; + rb = cb; + re = cb; + } + else if (m == mode::epoch) + m = mode::upstream; + + break; + } + default: + { + if (!digit (c) && !alpha (c)) + bad_arg ("alpha-numeric characters expected in a component"); + } + } + + if (!digit (c)) + lnn = p; + } + + assert (p >= cb); // 'p' denotes the end of the last component. + + // An empty component is valid for the release part, and for the upstream + // part when constructing empty or max limit version. + // + if (p == cb && m != mode::release && pr != parse::upstream) + bad_arg ("unexpected end"); + + // Parse the last component. + // + if (m == mode::revision) + { + if (lnn >= cb) // Contains non-digits. + bad_arg ("revision should be 2-byte unsigned integer"); + + revision = uint16 (cb, "revision"); + } + else if (cb != p) + { + canon_part->add (cb, p, lnn < cb); + + if (m == mode::epoch || m == mode::upstream) + ue = p; + else if (m == mode::release) + re = p; + } + + // Upstream and release pointer ranges are valid at the end of the day. + // + assert (ub <= ue && rb <= re); + + if (pr != parse::release) + { + // Fill upstream original and canonical parts. + // + if (!canon_upstream.empty ()) + { + assert (ub != ue); // Can't happen if through all previous checks. + canonical_upstream = canon_upstream.final (); + + if (pr == parse::full) + upstream.assign (ub, ue); + } + } + + if (pr != parse::upstream) + { + // Fill release original and canonical parts. + // + if (!canon_release.empty ()) + { + assert (rb != re); // Can't happen if through all previous checks. + canonical_release = canon_release.final (); + + if (pr == parse::full) + release = string (rb, re); + } + else + { + if (m == mode::release) + { + // Empty release part signifies the earliest possible version + // release. Make original, and keep canonical representations empty. + // + if (pr == parse::full) + release = ""; + } + else + { + // Absent release part signifies the final (max) version release. + // Assign the special value to the canonical representation, keep + // the original one nullopt. + // + canonical_release = "~"; + } + } + } + + if (pr == parse::full && epoch == 0 && canonical_upstream.empty () && + canonical_release.empty ()) + { + assert (revision == 0); // Can't happen if through all previous checks. + bad_arg ("empty version"); + } + } + + version& version:: + operator= (const version& v) + { + if (this != &v) + *this = version (v); // Reduce to move-assignment. + return *this; + } + + version& version:: + operator= (version&& v) + { + if (this != &v) + { + this->~version (); + new (this) version (move (v)); // Assume noexcept move-construction. + } + return *this; + } + + string version:: + string (bool ignore_revision) const + { + if (empty ()) + throw logic_error ("empty version"); + + std::string v (epoch != 0 ? to_string (epoch) + "~" + upstream : upstream); + + if (release) + { + v += '-'; + v += *release; + } + + if (!ignore_revision && revision != 0) + { + v += '+'; + v += to_string (revision); + } + + return v; + } + + // text_file + // + text_file:: + ~text_file () + { + if (file) + path.~path_type (); + else + text.~string (); + } + + text_file:: + text_file (text_file&& f): file (f.file), comment (move (f.comment)) + { + if (file) + new (&path) path_type (move (f.path)); + else + new (&text) string (move (f.text)); + } + + text_file:: + text_file (const text_file& f): file (f.file), comment (f.comment) + { + if (file) + new (&path) path_type (f.path); + else + new (&text) string (f.text); + } + + text_file& text_file:: + operator= (text_file&& f) + { + if (this != &f) + { + this->~text_file (); + new (this) text_file (move (f)); // Assume noexcept move-construction. + } + return *this; + } + + text_file& text_file:: + operator= (const text_file& f) + { + if (this != &f) + *this = text_file (f); // Reduce to move-assignment. + return *this; + } + + // depends + // + + dependency_constraint:: + dependency_constraint (optional mnv, bool mno, + optional mxv, bool mxo) + : min_version (move (mnv)), + max_version (move (mxv)), + min_open (mno), + max_open (mxo) + { + assert ( + // Min and max versions can't both be absent. + // + (min_version || max_version) && + + // Version should be non-empty. + // + (!min_version || !min_version->empty ()) && + (!max_version || !max_version->empty ()) && + + // Absent version endpoint (infinity) should be open. + // + (min_version || min_open) && (max_version || max_open)); + + if (min_version && max_version) + { + if (*min_version > *max_version) + throw invalid_argument ("min version is greater than max version"); + + if (*min_version == *max_version && (min_open || max_open)) + throw invalid_argument ("equal version endpoints not closed"); + } + } + + ostream& + operator<< (ostream& o, const dependency_constraint& c) + { + assert (!c.empty ()); + + if (!c.min_version) + return o << (c.max_open ? "< " : "<= ") << *c.max_version; + + if (!c.max_version) + return o << (c.min_open ? "> " : ">= ") << *c.min_version; + + if (*c.min_version == *c.max_version) + return o << "== " << *c.min_version; + + return o << (c.min_open ? '(' : '[') << *c.min_version << " " + << *c.max_version << (c.max_open ? ')' : ']'); + } + + ostream& + operator<< (ostream& o, const dependency& d) + { + o << d.name; + + if (d.constraint) + o << ' ' << *d.constraint; + + return o; + } + + ostream& + operator<< (ostream& o, const dependency_alternatives& as) + { + if (as.conditional) + o << '?'; + + if (as.buildtime) + o << '*'; + + if (as.conditional || as.buildtime) + o << ' '; + + bool f (true); + for (const dependency& a: as) + o << (f ? (f = false, "") : " | ") << a; + + if (!as.comment.empty ()) + o << "; " << as.comment; + + return o; + } + + // package_manifest + // + package_manifest:: + package_manifest (parser& p, bool iu) + : package_manifest (p, p.next (), false, iu) // Delegate + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single package manifest expected"); + } + + package_manifest:: + package_manifest (parser& p, name_value nv, bool iu) + : package_manifest (p, nv, true, iu) // Delegate + { + } + + package_manifest:: + package_manifest (parser& p, name_value nv, bool il, bool iu) + { + 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 (!nv.name.empty ()) + bad_name ("start of package manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + 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"); + + try + { + version = version_type (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid package version: ") + e.what ()); + } + + // Versions like 1.2.3- are forbidden in manifest as intended to be + // used for version constrains rather than actual releases. + // + if (version.release && version.release->empty ()) + bad_value ("invalid package version release"); + } + 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 = text_file (move (v)); + } + else if (n == "description-file") + { + if (il) + bad_name ("package description-file not allowed"); + + 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"); + + path p (v); + + if (p.absolute ()) + bad_value ("package description-file path is absolute"); + + description = text_file (move (p), 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") + { + if (il) + bad_name ("package changes-file not allowed"); + + string c (split_comment (v)); + + if (v.empty ()) + bad_value ("no path in package changes-file"); + + path p (v); + + if (p.absolute ()) + bad_value ("package changes-file path is absolute"); + + changes.emplace_back (move (p), 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 == "build-email") + { + if (build_email) + bad_name ("build email redefinition"); + + string c (split_comment (v)); + + build_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 (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") + { + // Allow specifying ?* in any order. + // + size_t m (v.size ()); + size_t cond ((m > 0 && v[0] == '?') || (m > 1 && v[1] == '?') ? 1 : 0); + size_t btim ((m > 0 && v[0] == '*') || (m > 1 && v[1] == '*') ? 1 : 0); + + requirement_alternatives ra (cond != 0, btim != 0, split_comment (v)); + string::const_iterator b (v.begin ()); + string::const_iterator e (v.end ()); + + if (ra.conditional || ra.buildtime) + { + string::size_type p (v.find_first_not_of (spaces, cond + btim)); + 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") + { + // Allow specifying ?* in any order. + // + size_t m (v.size ()); + size_t cond ((m > 0 && v[0] == '?') || (m > 1 && v[1] == '?') ? 1 : 0); + size_t btim ((m > 0 && v[0] == '*') || (m > 1 && v[1] == '*') ? 1 : 0); + + dependency_alternatives da (cond != 0, btim != 0, split_comment (v)); + string::const_iterator b (v.begin ()); + string::const_iterator e (v.end ()); + + if (da.conditional || da.buildtime) + { + string::size_type p (v.find_first_not_of (spaces, cond + btim)); + 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). + // + static const string cb ("=<>(["); + for (char c; i != e && cb.find (c = *i) == string::npos; ++i) + { + if (!space (c)) + ne = i + 1; + } + + if (i == e) + da.push_back (dependency {lv, nullopt}); + else + { + string nm (b, ne); + + if (nm.empty ()) + bad_value ("prerequisite package name not specified"); + + // Got to version range. + // + dependency_constraint dc; + const char* op (&*i); + char mnv (*op); + if (mnv == '(' || mnv == '[') + { + bool min_open (mnv == '('); + + string::size_type pos (lv.find_first_not_of (spaces, ++i - b)); + if (pos == string::npos) + bad_value ("no prerequisite package min version specified"); + + i = b + pos; + pos = lv.find_first_of (spaces, pos); + + static const char* no_max_version ( + "no prerequisite package max version specified"); + + if (pos == string::npos) + bad_value (no_max_version); + + version_type min_version; + + try + { + min_version = version_type (string (i, b + pos)); + } + catch (const invalid_argument& e) + { + bad_value ( + string ("invalid prerequisite package min version: ") + + e.what ()); + } + + pos = lv.find_first_not_of (spaces, pos); + if (pos == string::npos) + bad_value (no_max_version); + + i = b + pos; + static const string mve (spaces + "])"); + pos = lv.find_first_of (mve, pos); + + static const char* invalid_range ( + "invalid prerequisite package version range"); + + if (pos == string::npos) + bad_value (invalid_range); + + version_type max_version; + + try + { + max_version = version_type (string (i, b + pos)); + } + catch (const invalid_argument& e) + { + bad_value ( + string ("invalid prerequisite package max version: ") + + e.what ()); + } + + pos = lv.find_first_of ("])", pos); // Might be a space. + if (pos == string::npos) + bad_value (invalid_range); + + try + { + dc = dependency_constraint (move (min_version), + min_open, + move (max_version), + lv[pos] == ')'); + } + catch (const invalid_argument& e) + { + bad_value ( + string ("invalid dependency constraint: ") + e.what ()); + } + + if (lv[pos + 1] != '\0') + bad_value ( + "unexpected text after prerequisite package version range"); + } + else + { + // Version comparison notation. + // + enum comparison {eq, lt, gt, le, ge}; + comparison operation (eq); // Uninitialized warning. + + if (strncmp (op, "==", 2) == 0) + { + operation = eq; + i += 2; + } + else if (strncmp (op, ">=", 2) == 0) + { + operation = ge; + i += 2; + } + else if (strncmp (op, "<=", 2) == 0) + { + operation = le; + i += 2; + } + else if (*op == '>') + { + operation = gt; + ++i; + } + else if (*op == '<') + { + operation = 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"); + + version_type v; + + try + { + v = version_type (lv.c_str () + pos); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid prerequisite package version: ") + + e.what ()); + } + + switch (operation) + { + case comparison::eq: + dc = dependency_constraint (v); + break; + case comparison::lt: + dc = dependency_constraint (nullopt, true, move (v), true); + break; + case comparison::le: + dc = dependency_constraint (nullopt, true, move (v), false); + break; + case comparison::gt: + dc = dependency_constraint (move (v), true, nullopt, true); + break; + case comparison::ge: + dc = dependency_constraint (move (v), false, nullopt, true); + break; + } + } + + dependency d {move (nm), move (dc)}; + da.push_back (move (d)); + } + } + + if (da.empty ()) + bad_value ("empty package dependency specification"); + + dependencies.push_back (da); + } + else if (n == "location") + { + if (!il) + bad_name ("package location not allowed"); + + if (location) + bad_name ("package location redefinition"); + + try + { + path l (v); + + if (l.empty ()) + bad_value ("empty package location"); + + if (l.absolute ()) + bad_value ("absolute package location"); + + location = move (l); + } + catch (const invalid_path&) + { + bad_value ("invalid package location"); + } + } + else if (n == "sha256sum") + { + if (!il) + bad_name ("package sha256sum not allowed"); + + if (sha256sum) + bad_name ("package sha256sum redefinition"); + + if (!valid_sha256 (v)) + bad_value ("invalid package sha256sum"); + + sha256sum = move (v); + } + else if (!iu) + 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"); + + if (il) + { + if (!location) + bad_name ("no package location specified"); + + if (!sha256sum) + bad_name ("no package sha256sum specified"); + } + } + + void package_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified ? + // @@ Should we check that values are valid: name is not empty, version + // release is not empty, sha256sum is a proper string, ...? + // @@ Currently we don't know if we are serializing the individual package + // manifest or the package list manifest, so can't ensure all values + // allowed in the current context (location, sha256sum, *-file values). + // + + s.next ("", "1"); // Start of manifest. + s.next ("name", name); + s.next ("version", version.string ()); + + 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->path.string (), description->comment)); + else + s.next ("description", description->text); + } + + for (const auto& c: changes) + { + if (c.file) + s.next ("changes-file", add_comment (c.path.string (), c.comment)); + else + s.next ("changes", c.text); + } + + 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)); + + if (build_email) + s.next ("build-email", + add_comment (*build_email, build_email->comment)); + + for (const auto& d: dependencies) + s.next ("depends", + (d.conditional + ? (d.buildtime ? "?* " : "? ") + : (d.buildtime ? "* " : "")) + + add_comment (concatenate (d, " | "), d.comment)); + + for (const auto& r: requirements) + s.next ("requires", + (r.conditional + ? (r.buildtime ? "?* " : "? ") + : (r.buildtime ? "* " : "")) + + add_comment (concatenate (r, " | "), r.comment)); + + if (location) + s.next ("location", location->posix_string ()); + + if (sha256sum) + s.next ("sha256sum", *sha256sum); + + s.next ("", ""); // End of manifest. + } + + // package_manifests + // + package_manifests:: + package_manifests (parser& p, bool iu) + { + name_value nv (p.next ()); + + 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 (!nv.name.empty ()) + bad_name ("start of package list manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the package list manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "sha256sum") + { + if (!sha256sum.empty ()) + bad_name ("sha256sum redefinition"); + + if (!valid_sha256 (v)) + bad_value ("invalid sha256sum"); + + sha256sum = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in package list manifest"); + } + + // Verify all non-optional values were specified. + // + if (sha256sum.empty ()) + bad_value ("no sha256sum specified"); + + // Parse package manifests. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + push_back (package_manifest (p, nv, iu)); + } + + void package_manifests:: + serialize (serializer& s) const + { + // Serialize the package list manifest. + // + // @@ Should we check that values are valid ? + // + s.next ("", "1"); // Start of manifest. + s.next ("sha256sum", sha256sum); + s.next ("", ""); // End of manifest. + + // Serialize package manifests. + // + for (const package_manifest& p: *this) + { + auto bad_value = [&p, &s](const string& d) + { + throw + serialization ( + s.name (), d + " for " + p.name + "-" + p.version.string ()); + }; + + if (p.description && p.description->file) + bad_value ("forbidden description-file"); + + for (const auto& c: p.changes) + if (c.file) + bad_value ("forbidden changes-file"); + + if (!p.location) + bad_value ("no valid location"); + + if (!p.sha256sum) + bad_value ("no valid sha256sum"); + + p.serialize (s); + } + + s.next ("", ""); // End of stream. + } + + // url_parts + // + struct url_parts + { + using protocol = repository_location::protocol; + + protocol proto; + string host; + uint16_t port; + dir_path path; + + explicit + url_parts (const string&); + }; + + // Return the URL protocol, or nullopt if location is not a URL. + // + static optional + is_url (const string& location) + { + using protocol = url_parts::protocol; + + optional p; + if (casecmp (location, "http://", 7) == 0) + p = protocol::http; + else if (casecmp (location, "https://", 8) == 0) + p = protocol::https; + + return p; + } + + static string + to_string (url_parts::protocol proto, + const string& host, + uint16_t port, + const dir_path& path) + { + string u ( + (proto == url_parts::protocol::http ? "http://" : "https://") + host); + + if (port != 0) + u += ":" + std::to_string (port); + + if (!path.empty ()) + u += "/" + path.posix_string (); + + return u; + } + + url_parts:: + url_parts (const string& s) + { + optional pr (is_url (s)); + if (!pr) + throw invalid_argument ("invalid protocol"); + + proto = *pr; + + string::size_type host_offset (s.find ("//")); + assert (host_offset != string::npos); + host_offset += 2; + + string::size_type p (s.find ('/', host_offset)); + + if (p != string::npos) + // Chop the path part. Path is saved as a relative one to be of the + // same type on different operating systems including Windows. + // + path = dir_path (s, p + 1, string::npos); + + // Put the lower-cased version of the host part into host. + // Chances are good it will stay unmodified. + // + transform (s.cbegin () + host_offset, + p == string::npos ? s.cend () : s.cbegin () + p, + back_inserter (host), + static_cast (lcase)); + + // Validate host name according to "2.3.1. Preferred name syntax" and + // "2.3.4. Size limits" of https://tools.ietf.org/html/rfc1035. + // + // Check that there is no empty labels and ones containing chars + // different from alpha-numeric and hyphen. Label should start from + // letter, do not end with hypen and be not longer than 63 chars. + // Total host name length should be not longer than 255 chars. + // + auto hb (host.cbegin ()); + auto he (host.cend ()); + auto ls (hb); // Host domain name label begin. + auto pt (he); // Port begin. + + for (auto i (hb); i != he; ++i) + { + char c (*i); + + if (pt == he) // Didn't reach port specification yet. + { + if (c == ':') // Port specification reached. + pt = i; + else + { + auto n (i + 1); + + // Validate host name. + // + + // Is first label char. + // + bool flc (i == ls); + + // Is last label char. + // + bool llc (n == he || *n == '.' || *n == ':'); + + // Validate char. + // + bool valid (alpha (c) || + (digit (c) && !flc) || + ((c == '-' || c == '.') && !flc && !llc)); + + // Validate length. + // + if (valid) + valid = i - ls < 64 && i - hb < 256; + + if (!valid) + throw invalid_argument ("invalid host"); + + if (c == '.') + ls = n; + } + } + else + { + // Validate port. + // + if (!digit (c)) + throw invalid_argument ("invalid port"); + } + } + + // Chop the port, if present. + // + if (pt == he) + port = 0; + else + { + unsigned long long n (++pt == he ? 0 : stoull (string (pt, he))); + if (n == 0 || n > UINT16_MAX) + throw invalid_argument ("invalid port"); + + port = static_cast (n); + host.resize (pt - hb - 1); + } + + if (host.empty ()) + throw invalid_argument ("invalid host"); + } + + // repository_location + // + static string + strip_domain (const string& host) + { + assert (!host.empty ()); // Should be repository location host. + + string h; + bool bpkg (false); + + if (host.compare (0, 4, "www.") == 0 || + host.compare (0, 4, "pkg.") == 0 || + (bpkg = host.compare (0, 5, "bpkg.") == 0)) + { + if (h.assign (host, bpkg ? 5 : 4, string::npos).empty ()) + throw invalid_argument ("invalid host"); + } + else + h = host; + + return h; + } + + // The 'pkg' path component stripping mode. + // + enum class strip_mode {version, component, path}; + + static dir_path + strip_path (const dir_path& path, strip_mode mode) + { + // Should be repository location path. + // + assert (!path.empty () && *path.begin () != ".."); + + auto rb (path.rbegin ()), i (rb), re (path.rend ()); + + // Find the version component. + // + for (; i != re; ++i) + { + const string& c (*i); + + if (!c.empty () && c.find_first_not_of ("1234567890") == string::npos) + break; + } + + if (i == re) + throw invalid_argument ("missing repository version"); + + // Validate the version. At the moment the only valid value is 1. + // + if (stoul (*i) != 1) + throw invalid_argument ("unsupported repository version"); + + dir_path res (rb, i); + + // Canonical name prefix part ends with the special "pkg" component. + // + bool pc (++i != re && (*i == "pkg" || *i == "bpkg")); + + if (pc && mode == strip_mode::component) + ++i; // Strip the "pkg" component. + + if (!pc || mode != strip_mode::path) + res = dir_path (i, re) / res; // Concatenate prefix and path parts. + + return res; + } + + // Location parameter type is fully qualified as compiler gets confused with + // string() member. + // + repository_location:: + repository_location (const std::string& l) + : repository_location (l, repository_location ()) // Delegate. + { + if (!empty () && relative ()) + throw invalid_argument ("relative filesystem path"); + } + + repository_location:: + repository_location (const std::string& l, const repository_location& b) + { + // Otherwise compiler gets confused with string() member. + // + using std::string; + + if (l.empty ()) + { + if (!b.empty ()) + throw invalid_argument ("empty location"); + + return; + } + + // Base repository location can not be a relative path. + // + if (!b.empty () && b.relative ()) + throw invalid_argument ("base relative filesystem path"); + + if (is_url (l)) + { + url_parts u (l); + proto_ = u.proto; + host_ = move (u.host); + port_ = u.port; + path_ = move (u.path); + + canonical_name_ = strip_domain (host_); + + // For canonical name and for the HTTP protocol, treat a.com and + // a.com:80 as the same name. The same rule applies to the HTTPS + // protocol and port 443. + // + if (port_ != 0 && port_ != (proto_ == protocol::http ? 80 : 443)) + canonical_name_ += ':' + std::to_string (port_); + } + else + { + path_ = dir_path (l); + + // Complete if we are relative and have base. + // + if (!b.empty () && path_.relative ()) + { + // Convert the relative path location to an absolute or remote one. + // + proto_ = b.proto_; + host_ = b.host_; + port_ = b.port_; + path_ = b.path_ / path_; + + // Set canonical name to the base location canonical name host + // part. The path part of the canonical name is calculated below. + // + if (b.remote ()) + canonical_name_ = + b.canonical_name_.substr (0, b.canonical_name_.find ("/")); + } + } + + // Normalize path to avoid different representations of the same location + // and canonical name. So a/b/../c/1/x/../y and a/c/1/y to be considered + // as same location. + // + try + { + path_.normalize (); + } + catch (const invalid_path&) + { + throw invalid_argument ("invalid path"); + } + + // Need to check path for emptiness before proceeding further as a valid + // non empty location can not have an empty path_ member (which can be the + // case for the remote location, but not for the relative or absolute). + // + if (path_.empty ()) + throw invalid_argument ("empty path"); + + // Need to check that URL path do not go past the root directory of a WEB + // server. We can not rely on the above normalize() function call doing + // this check as soon as path_ member contains a relative directory for the + // remote location. + // + if (remote () && *path_.begin () == "..") + throw invalid_argument ("invalid path"); + + // Finish calculating the canonical name, unless we are relative. + // + if (relative ()) + { + assert (canonical_name_.empty ()); + return; + } + + // Canonical name / part. + // + dir_path sp (strip_path ( + path_, remote () ? strip_mode::component : strip_mode::path)); + + // If for an absolute path location the stripping result is empty (which + // also means part is empty as well) then fallback to stripping + // just the version component. + // + if (absolute () && sp.empty ()) + sp = strip_path (path_, strip_mode::version); + + string cp (sp.relative () ? sp.posix_string () : sp.string ()); + + // Note: allow empty paths (e.g., http://stable.cppget.org/1/). + // + if (!canonical_name_.empty () && !cp.empty ()) // If we have host and dir. + canonical_name_ += '/'; + + canonical_name_ += cp; + + // But don't allow empty canonical names. + // + if (canonical_name_.empty ()) + throw invalid_argument ("empty repository name"); + } + + string repository_location:: + string () const + { + if (empty ()) + return std::string (); // Also function name. + + if (local ()) + return relative () ? path_.posix_string () : path_.string (); + + return to_string (proto_, host_, port_, path_); + } + + // repository_manifest + // + repository_manifest:: + repository_manifest (parser& p, bool iu) + : repository_manifest (p, p.next (), iu) // Delegate + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single repository manifest expected"); + } + + repository_manifest:: + repository_manifest (parser& p, name_value nv, bool iu) + { + 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 (!nv.name.empty ()) + bad_name ("start of repository manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "location") + { + if (!location.empty ()) + bad_name ("location redefinition"); + + if (v.empty ()) + bad_value ("empty location"); + + try + { + // Call prerequisite repository location constructor, do not + // ammend relative path. + // + location = repository_location (move (v), repository_location ()); + } + catch (const invalid_argument& e) + { + bad_value (e.what ()); + } + } + else if (n == "role") + { + if (role) + bad_name ("role redefinition"); + + auto b (repository_role_names.cbegin ()); + auto e (repository_role_names.cend ()); + auto i (find (b, e, v)); + + if (i == e) + bad_value ("unrecognized role"); + + role = static_cast (i - b); + } + else if (n == "url") + { + if (url) + bad_name ("url redefinition"); + + if (v.empty ()) + bad_value ("empty url"); + + url = move (v); + } + else if (n == "email") + { + if (email) + bad_name ("email redefinition"); + + string c (split_comment (v)); + + if (v.empty ()) + bad_value ("empty email"); + + email = email_type (move (v), move (c)); + } + else if (n == "summary") + { + if (summary) + bad_name ("summary redefinition"); + + if (v.empty ()) + bad_value ("empty summary"); + + summary = move (v); + } + else if (n == "description") + { + if (description) + bad_name ("description redefinition"); + + if (v.empty ()) + bad_value ("empty description"); + + description = move (v); + } + else if (n == "certificate") + { + if (certificate) + bad_name ("certificate redefinition"); + + if (v.empty ()) + bad_value ("empty certificate"); + + certificate = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in repository manifest"); + } + + // Verify all non-optional values were specified. + // + // - location can be omitted + // - role can be omitted + // + if (role && location.empty () != (*role == repository_role::base)) + bad_value ("invalid role"); + + if (effective_role () != repository_role::base) + { + if (url) + bad_value ("url not allowed"); + + if (email) + bad_value ("email not allowed"); + + if (summary) + bad_value ("summary not allowed"); + + if (description) + bad_value ("description not allowed"); + + if (certificate) + bad_value ("certificate not allowed"); + } + } + + void repository_manifest:: + serialize (serializer& s) const + { + auto bad_value ([&s](const string& d) { + throw serialization (s.name (), d);}); + + s.next ("", "1"); // Start of manifest. + + if (!location.empty ()) + s.next ("location", location.string ()); + + if (role) + { + if (location.empty () != (*role == repository_role::base)) + bad_value ("invalid role"); + + auto r (static_cast (*role)); + assert (r < repository_role_names.size ()); + s.next ("role", repository_role_names[r]); + } + + bool b (effective_role () == repository_role::base); + + if (url) + { + if (!b) + bad_value ("url not allowed"); + + s.next ("url", *url); + } + + if (email) + { + if (!b) + bad_value ("email not allowed"); + + s.next ("email", add_comment (*email, email->comment)); + } + + if (summary) + { + if (!b) + bad_value ("summary not allowed"); + + s.next ("summary", *summary); + } + + if (description) + { + if (!b) + bad_value ("description not allowed"); + + s.next ("description", *description); + } + + if (certificate) + { + if (!b) + bad_value ("certificate not allowed"); + + s.next ("certificate", *certificate); + } + + s.next ("", ""); // End of manifest. + } + + repository_role repository_manifest:: + effective_role () const + { + if (role) + { + if (location.empty () != (*role == repository_role::base)) + throw logic_error ("invalid role"); + + return *role; + } + else + return location.empty () + ? repository_role::base + : repository_role::prerequisite; + } + + optional repository_manifest:: + effective_url (const repository_location& l) const + { + if (!url || (*url)[0] != '.') + return url; + + const dir_path rp (*url); + auto i (rp.begin ()); + + static const char* invalid_url ("invalid relative url"); + + auto strip ([&i, &rp]() -> bool { + if (i != rp.end ()) + { + const auto& c (*i++); + if (c == "..") + return true; + + if (c == ".") + return false; + } + + throw invalid_argument (invalid_url); + }); + + bool strip_d (strip ()); // Strip domain. + bool strip_p (strip ()); // Strip path. + + // The web interface relative path with the special first two components + // stripped. + // + const dir_path rpath (i, rp.end ()); + assert (rpath.relative ()); + + url_parts u (l.string ()); + + // Web interface URL path part. + // + // It is important to call strip_path() before appending the relative + // path. Otherwise the effective URL for the path ./../../.. and the + // repository location http://a.com/foo/pkg/1/math will wrongly be + // http://a.com/foo/pkg instead of http://a.com. + // + dir_path ipath ( + strip_path ( + u.path, + strip_p ? strip_mode::component : strip_mode::version) / rpath); + + static const char* invalid_location ("invalid repository location"); + + try + { + ipath.normalize (false, true); // Current dir collapses to an empty one. + } + catch (const invalid_path&) + { + throw invalid_argument (invalid_location); + } + + assert (ipath.relative ()); + + if (!ipath.empty () && *ipath.begin () == "..") + throw invalid_argument (invalid_location); + + return to_string ( + u.proto, strip_d ? strip_domain (u.host) : u.host, u.port, ipath); + } + + // repository_manifests + // + repository_manifests:: + repository_manifests (parser& p, bool iu) + { + name_value nv (p.next ()); + while (!nv.empty ()) + { + push_back (repository_manifest (p, nv, iu)); + nv = p.next (); + + // Make sure there is location in all except the last entry. + // + if (back ().location.empty () && !nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "repository location expected"); + } + + if (empty () || !back ().location.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "base repository manifest expected"); + } + + void repository_manifests:: + serialize (serializer& s) const + { + if (empty () || !back ().location.empty ()) + throw serialization (s.name (), "base repository manifest expected"); + + // @@ Should we check that there is location in all except the last + // entry? + // + for (const repository_manifest& r: *this) + r.serialize (s); + + s.next ("", ""); // End of stream. + } + + // signature_manifest + // + signature_manifest:: + signature_manifest (parser& p, bool iu) + : signature_manifest (p, p.next (), iu) // Delegate + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single signature manifest expected"); + } + + signature_manifest:: + signature_manifest (parser& p, name_value nv, bool iu) + { + 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 (!nv.name.empty ()) + bad_name ("start of signature manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "sha256sum") + { + if (!sha256sum.empty ()) + bad_name ("sha256sum redefinition"); + + if (v.empty ()) + bad_value ("empty sha256sum"); + + if (!valid_sha256 (v)) + bad_value ("invalid sha256sum"); + + sha256sum = move (v); + } + else if (n == "signature") + { + if (!signature.empty ()) + bad_name ("signature redefinition"); + + if (v.empty ()) + bad_value ("empty signature"); + + // Try to base64-decode as a sanity check. + // + try + { + signature = base64_decode (v); + } + catch (const invalid_argument&) + { + bad_value ("invalid signature"); + } + } + else if (!iu) + bad_name ("unknown name '" + n + "' in signature manifest"); + } + + // Verify all non-optional values were specified. + // + if (sha256sum.empty ()) + bad_value ("no sha256sum specified"); + else if (signature.empty ()) + bad_value ("no signature specified"); + + // Make sure this is the end. + // + nv = p.next (); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single signature manifest expected"); + } + + void signature_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that values are valid ? + // + s.next ("", "1"); // Start of manifest. + + s.next ("sha256sum", sha256sum); + s.next ("signature", base64_encode (signature)); + + s.next ("", ""); // End of manifest. + } +} diff --git a/libbpkg/manifest.hxx b/libbpkg/manifest.hxx new file mode 100644 index 0000000..1b8c694 --- /dev/null +++ b/libbpkg/manifest.hxx @@ -0,0 +1,636 @@ +// file : libbpkg/manifest.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBPKG_MANIFEST_HXX +#define LIBBPKG_MANIFEST_HXX + +#include +#include +#include +#include // uint16_t +#include +#include // move() +#include // logic_error + +#include +#include +#include + +#include +#include + +namespace bpkg +{ + using strings = std::vector; + + // @@ Let's create with "basic" package types. + // + class LIBBPKG_EXPORT version + { + public: + // Let's keep the members in the order they appear in the string + // representation. + // + const std::uint16_t epoch; + const std::string upstream; + const butl::optional release; + const std::uint16_t revision; + + // Upstream part canonical representation. + // + const std::string canonical_upstream; + + // Release part canonical representation. + // + const std::string canonical_release; + + // Create a special empty version. It is less than any other valid + // version (and is conceptually equivalent to 0-). + // + version (): epoch (0), release (""), revision (0) {} + + // Throw std::invalid_argument if the passed string is not a valid + // version representation. + // + explicit + version (const std::string& v): version (v.c_str ()) {} + + explicit + version (const char* v): version (data_type (v, data_type::parse::full)) {} + + // Create the version object from separate epoch, upstream, release, and + // revision parts. + // + // Note that it is possible (and legal) to create the special empty + // version via this interface as version(0, string(), string(), 0). + // + version (std::uint16_t epoch, + std::string upstream, + butl::optional release, + std::uint16_t revision); + + version (version&&) = default; + version (const version&) = default; + version& operator= (version&&); + version& operator= (const version&); + + std::string + string (bool ignore_revision = false) const; + + 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_upstream.compare (v.canonical_upstream)) + return c; + + if (int c = canonical_release.compare (v.canonical_release)) + return c; + + if (!ignore_revision && revision != v.revision) + return revision < v.revision ? -1 : 1; + + return 0; + } + + bool + empty () const noexcept + { + bool e (upstream.empty ()); + assert (!e || + (epoch == 0 && release && release->empty () && revision == 0)); + return e; + } + + private: + struct LIBBPKG_EXPORT data_type + { + enum class parse {full, upstream, release}; + + data_type (const char*, parse); + + std::uint16_t epoch; + std::string upstream; + butl::optional release; + std::uint16_t revision; + std::string canonical_upstream; + std::string canonical_release; + }; + + explicit + version (data_type&& d) + : epoch (d.epoch), + upstream (std::move (d.upstream)), + release (std::move (d.release)), + revision (d.revision), + canonical_upstream (std::move (d.canonical_upstream)), + canonical_release (std::move (d.canonical_release)) {} + }; + + inline std::ostream& + operator<< (std::ostream& os, const version& v) + { + return os << (v.empty () ? "" : v.string ()); + } + + // priority + // + class priority + { + 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, std::string c = "") + : value (v), comment (std::move (c)) {} + + operator value_type () const {return value;} + }; + + // description + // description-file + // change + // change-file + // + class LIBBPKG_EXPORT text_file + { + public: + using path_type = butl::path; + + bool file; + + union + { + std::string text; + path_type path; + }; + + std::string comment; + + // File text constructor. + // + explicit + text_file (std::string t = ""): file (false), text (std::move (t)) {} + + // File reference constructor. + // + text_file (path_type p, std::string c) + : file (true), path (std::move (p)), comment (std::move (c)) {} + + text_file (text_file&&); + text_file (const text_file&); + text_file& operator= (text_file&&); + text_file& operator= (const text_file&); + + ~text_file (); + }; + + // license + // + class licenses: public strings + { + public: + std::string comment; + + explicit + licenses (std::string c = ""): comment (std::move (c)) {} + }; + + // url + // package-url + // + class url: public std::string + { + public: + std::string comment; + + explicit + url (std::string u = "", std::string c = "") + : std::string (std::move (u)), comment (std::move (c)) {} + }; + + // email + // package-email + // build-email + // + class email: public std::string + { + public: + std::string comment; + + explicit + email (std::string e = "", std::string c = "") + : std::string (std::move (e)), comment (std::move (c)) {} + }; + + // depends + // + struct LIBBPKG_EXPORT dependency_constraint + { + butl::optional min_version; + butl::optional max_version; + bool min_open; + bool max_open; + + dependency_constraint (butl::optional min_version, bool min_open, + butl::optional max_version, bool max_open); + + dependency_constraint (const version& v) + : dependency_constraint (v, false, v, false) {} + + dependency_constraint () = default; + + bool + empty () const noexcept {return !min_version && !max_version;} + }; + + LIBBPKG_EXPORT std::ostream& + operator<< (std::ostream&, const dependency_constraint&); + + inline bool + operator== (const dependency_constraint& x, const dependency_constraint& y) + { + return x.min_version == y.min_version && x.max_version == y.max_version && + x.min_open == y.min_open && x.max_open == y.max_open; + } + + inline bool + operator!= (const dependency_constraint& x, const dependency_constraint& y) + { + return !(x == y); + } + + struct dependency + { + std::string name; + butl::optional constraint; + }; + + LIBBPKG_EXPORT std::ostream& + operator<< (std::ostream&, const dependency&); + + class dependency_alternatives: public std::vector + { + public: + bool conditional; + bool buildtime; + std::string comment; + + dependency_alternatives () = default; + dependency_alternatives (bool d, bool b, std::string c) + : conditional (d), buildtime (b), comment (std::move (c)) {} + }; + + LIBBPKG_EXPORT std::ostream& + operator<< (std::ostream&, const dependency_alternatives&); + + // requires + // + class requirement_alternatives: public strings + { + public: + bool conditional; + bool buildtime; + std::string comment; + + requirement_alternatives () = default; + requirement_alternatives (bool d, bool b, std::string c) + : conditional (d), buildtime (b), comment (std::move (c)) {} + }; + + class LIBBPKG_EXPORT package_manifest + { + public: + using version_type = bpkg::version; + using priority_type = bpkg::priority; + using url_type = bpkg::url; + using email_type = bpkg::email; + + std::string name; + version_type version; + butl::optional priority; + std::string summary; + std::vector license_alternatives; + strings tags; + butl::optional description; + std::vector changes; + url_type url; + butl::optional package_url; + email_type email; + butl::optional package_email; + butl::optional build_email; + std::vector dependencies; + std::vector requirements; + + // The following values are only valid in the manifest list. + // + butl::optional location; + butl::optional sha256sum; + + public: + package_manifest () = default; // VC export. + + // Create individual package manifest. + // + package_manifest (butl::manifest_parser&, bool ignore_unknown = false); + + // Create an element of the package list manifest. + // + package_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + + private: + package_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool in_list, + bool ignore_unknown); + }; + + class LIBBPKG_EXPORT package_manifests: public std::vector + { + public: + using base_type = std::vector; + + using base_type::base_type; + + // Checksum of the corresponding repository_manifests. + // + std::string sha256sum; + + public: + package_manifests () = default; + package_manifests (butl::manifest_parser&, bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + class LIBBPKG_EXPORT repository_location + { + public: + // Create a special empty repository_location. + // + repository_location () = default; + + // If the argument is not empty, create remote/absolute repository + // location. Throw std::invalid_argument if the location is a relative + // path. If the argument is empty, then create the special empty + // location. + // + explicit + repository_location (const std::string&); + + // Create a potentially relative repository location. If base is not + // empty, use it to complete the relative location to remote/absolute. + // Throw std::invalid_argument if base is not empty but the location is + // empty, base itself is relative, or the resulting completed location + // is invalid. + // + repository_location (const std::string&, const repository_location& base); + + repository_location (const repository_location& l, + const repository_location& base) + : repository_location (l.string (), base) {} + + // Note that relative locations have no canonical name. Canonical + // name of an empty location is the empty name. + // + const std::string& + canonical_name () const noexcept {return canonical_name_;} + + // There are 3 types of locations: remote, local absolute filesystem + // path and local relative filesystem path. Plus there is the special + // empty location. The following predicates can be used to determine + // what kind of location it is. Note that except for empty(), all the + // other predicates throw std::logic_error for an empty location. + // + bool + empty () const noexcept {return path_.empty ();} + + bool + local () const + { + if (empty ()) + throw std::logic_error ("empty location"); + + return host_.empty (); + } + + bool + remote () const + { + return !local (); + } + + bool + absolute () const + { + if (empty ()) + throw std::logic_error ("empty location"); + + // Note that in remote locations path is always relative. + // + return path_.absolute (); + } + + bool + relative () const + { + return local () && path_.relative (); + } + + const butl::dir_path& + path () const + { + if (empty ()) + throw std::logic_error ("empty location"); + + return path_; + } + + const std::string& + host () const + { + if (local ()) + throw std::logic_error ("local location"); + + return host_; + } + + // Value 0 indicated that no port was specified explicitly. + // + std::uint16_t + port () const + { + if (local ()) + throw std::logic_error ("local location"); + + return port_; + } + + enum class protocol {http, https}; + + protocol + proto () const + { + if (local ()) + throw std::logic_error ("local location"); + + return proto_; + } + + // Note that this is not necessarily syntactically the same string + // as what was used to initialize this location. But it should be + // semantically equivalent. String representation of an empty + // location is the empty string. + // + std::string + string () const; + + private: + std::string canonical_name_; + protocol proto_; + std::string host_; + std::uint16_t port_; + butl::dir_path path_; + }; + + inline std::ostream& + operator<< (std::ostream& os, const repository_location& l) + { + return os << l.string (); + } + + enum class repository_role + { + base, + prerequisite, + complement + }; + + class LIBBPKG_EXPORT repository_manifest + { + public: + using email_type = bpkg::email; + + repository_location location; + butl::optional role; + + // The following values may only be present for the base repository. + // + butl::optional url; + butl::optional email; + butl::optional summary; + butl::optional description; + butl::optional certificate; + + // Return the effective role of the repository. If the role is not + // explicitly specified (see the role member above), then calculate + // the role based on the location. Specifically, if the location is + // empty, then the effective role is base. Otherwise -- prerequisite. + // If the role is specified, then verify that it is consistent with + // the location value (that is, base if the location is empty and + // prerequisite or complement if not) and return that. Otherwise, + // throw std::logic_error. + // + repository_role + effective_role () const; + + // Return the effective web interface URL based on the specified remote + // repository location. If url is not present or doesn't start with '.', + // then return it unchanged. Otherwise, process the relative format + // as described in the manifest specification. Throw std::invalid_argument + // if the relative url format is invalid or if the repository location is + // empty or local. + // + butl::optional + effective_url (const repository_location&) const; + + public: + repository_manifest () = default; // VC export. + repository_manifest (butl::manifest_parser&, bool ignore_unknown = false); + repository_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + class LIBBPKG_EXPORT repository_manifests: + public std::vector + { + public: + using base_type = std::vector; + + using base_type::base_type; + + repository_manifests () = default; + repository_manifests (butl::manifest_parser&, bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + class LIBBPKG_EXPORT signature_manifest + { + public: + // Checksum of the corresponding package_manifests. + // + std::string sha256sum; + + // Signature of the corresponding package_manifests. Calculated by + // encrypting package_manifests checksum (stored in sha256sum) with the + // repository certificate private key. + // + std::vector signature; + + public: + signature_manifest () = default; + signature_manifest (butl::manifest_parser&, bool ignore_unknown = false); + + // Serialize sha256sum and base64-encoded representation of the signature. + // + void + serialize (butl::manifest_serializer&) const; + + private: + // Used for delegating in public constructor. Strictly speaking is not + // required, as a signature_manifest currently never appears as a part of + // a manifest list, but kept for the consistency with other manifests + // implementations. + // + signature_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool ignore_unknown); + }; +} + +#endif // LIBBPKG_MANIFEST_HXX diff --git a/libbpkg/version.hxx.in b/libbpkg/version.hxx.in new file mode 100644 index 0000000..26c6d95 --- /dev/null +++ b/libbpkg/version.hxx.in @@ -0,0 +1,44 @@ +// file : libbpkg/version.hxx.in -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBPKG_VERSION // Note: using the version macro itself. + +// Note: using build2 standard versioning scheme. The numeric version format +// is AAABBBCCCDDDE where: +// +// AAA - major version number +// BBB - minor version number +// CCC - bugfix 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 +// +#define LIBBPKG_VERSION $libbpkg.version.project_number$ULL +#define LIBBPKG_VERSION_STR "$libbpkg.version.project$" +#define LIBBPKG_VERSION_ID "$libbpkg.version.project_id$" + +#define LIBBPKG_VERSION_MAJOR $libbpkg.version.major$ +#define LIBBPKG_VERSION_MINOR $libbpkg.version.minor$ +#define LIBBPKG_VERSION_PATCH $libbpkg.version.patch$ + +#define LIBBPKG_PRE_RELEASE $libbpkg.version.pre_release$ + +#define LIBBPKG_SNAPSHOT $libbpkg.version.snapshot_sn$ULL +#define LIBBPKG_SNAPSHOT_ID "$libbpkg.version.snapshot_id$" + +#include + +$libbutl.check(LIBBUTL_VERSION, LIBBUTL_SNAPSHOT)$ + +#endif // LIBBPKG_VERSION -- cgit v1.1