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 --- bpkg/.gitignore | 1 - bpkg/buildfile | 36 - bpkg/export | 41 - bpkg/manifest | 636 ---------- bpkg/manifest.cxx | 2190 ---------------------------------- bpkg/version.in | 44 - build/export.build | 4 +- build/root.build | 2 +- buildfile | 2 +- libbpkg/.gitignore | 1 + libbpkg/buildfile | 36 + libbpkg/export.hxx | 41 + libbpkg/manifest.cxx | 2190 ++++++++++++++++++++++++++++++++++ libbpkg/manifest.hxx | 636 ++++++++++ libbpkg/version.hxx.in | 44 + tests/.gitignore | 2 + tests/manifest/buildfile | 4 +- tests/manifest/driver.cxx | 2 +- tests/package-version/buildfile | 4 +- tests/package-version/driver.cxx | 2 +- tests/repository-location/buildfile | 4 +- tests/repository-location/driver.cxx | 2 +- 22 files changed, 2963 insertions(+), 2961 deletions(-) delete mode 100644 bpkg/.gitignore delete mode 100644 bpkg/buildfile delete mode 100644 bpkg/export delete mode 100644 bpkg/manifest delete mode 100644 bpkg/manifest.cxx delete mode 100644 bpkg/version.in 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 diff --git a/bpkg/.gitignore b/bpkg/.gitignore deleted file mode 100644 index 088eda4..0000000 --- a/bpkg/.gitignore +++ /dev/null @@ -1 +0,0 @@ -version diff --git a/bpkg/buildfile b/bpkg/buildfile deleted file mode 100644 index ae87591..0000000 --- a/bpkg/buildfile +++ /dev/null @@ -1,36 +0,0 @@ -# file : bpkg/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 bpkg/ subdirectory of, say, /usr/include/. -# -install.include = $install.include/bpkg/ diff --git a/bpkg/export b/bpkg/export deleted file mode 100644 index fadf41d..0000000 --- a/bpkg/export +++ /dev/null @@ -1,41 +0,0 @@ -// file : bpkg/export -*- C++ -*- -// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BPKG_EXPORT -#define BPKG_EXPORT - -// 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 // BPKG_EXPORT diff --git a/bpkg/manifest b/bpkg/manifest deleted file mode 100644 index d6caabf..0000000 --- a/bpkg/manifest +++ /dev/null @@ -1,636 +0,0 @@ -// file : bpkg/manifest -*- C++ -*- -// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BPKG_MANIFEST -#define BPKG_MANIFEST - -#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 // BPKG_MANIFEST diff --git a/bpkg/manifest.cxx b/bpkg/manifest.cxx deleted file mode 100644 index ef86b99..0000000 --- a/bpkg/manifest.cxx +++ /dev/null @@ -1,2190 +0,0 @@ -// file : bpkg/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/bpkg/version.in b/bpkg/version.in deleted file mode 100644 index 113c9bf..0000000 --- a/bpkg/version.in +++ /dev/null @@ -1,44 +0,0 @@ -// file : bpkg/version.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 diff --git a/build/export.build b/build/export.build index 58b6799..6e1378c 100644 --- a/build/export.build +++ b/build/export.build @@ -4,7 +4,7 @@ $out_root/: { - include bpkg/ + include libbpkg/ } -export $out_root/bpkg/lib{bpkg} +export $out_root/libbpkg/lib{bpkg} diff --git a/build/root.build b/build/root.build index af2c962..196d2a3 100644 --- a/build/root.build +++ b/build/root.build @@ -6,7 +6,7 @@ cxx.std = latest using cxx -hxx{*}: extension = +hxx{*}: extension = hxx ixx{*}: extension = ixx txx{*}: extension = txx cxx{*}: extension = cxx diff --git a/buildfile b/buildfile index d8b472e..04eb17f 100644 --- a/buildfile +++ b/buildfile @@ -2,7 +2,7 @@ # copyright : Copyright (c) 2014-2017 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -./: bpkg/ tests/ doc{INSTALL LICENSE NEWS README version} file{manifest} +./: libbpkg/ tests/ doc{INSTALL LICENSE NEWS README version} file{manifest} doc{version}: file{manifest} # Generated by the version module. doc{version}: dist = true 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 diff --git a/tests/.gitignore b/tests/.gitignore index e54525b..2e508a9 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,3 @@ driver +test/ +test-*/ diff --git a/tests/manifest/buildfile b/tests/manifest/buildfile index c35024c..ddc5938 100644 --- a/tests/manifest/buildfile +++ b/tests/manifest/buildfile @@ -4,6 +4,6 @@ import libs += libbutl%lib{butl} -exe{driver}: cxx{driver} ../../bpkg/lib{bpkg} $libs test{testscript} +exe{driver}: cxx{driver} ../../libbpkg/lib{bpkg} $libs test{testscript} -include ../../bpkg/ +include ../../libbpkg/ diff --git a/tests/manifest/driver.cxx b/tests/manifest/driver.cxx index 2b93e5a..157546e 100644 --- a/tests/manifest/driver.cxx +++ b/tests/manifest/driver.cxx @@ -10,7 +10,7 @@ #include #include -#include +#include using namespace std; using namespace butl; diff --git a/tests/package-version/buildfile b/tests/package-version/buildfile index 9a47c0b..422e92a 100644 --- a/tests/package-version/buildfile +++ b/tests/package-version/buildfile @@ -4,6 +4,6 @@ import libs += libbutl%lib{butl} -exe{driver}: cxx{driver} ../../bpkg/lib{bpkg} $libs +exe{driver}: cxx{driver} ../../libbpkg/lib{bpkg} $libs -include ../../bpkg/ +include ../../libbpkg/ diff --git a/tests/package-version/driver.cxx b/tests/package-version/driver.cxx index 60460ab..4659db9 100644 --- a/tests/package-version/driver.cxx +++ b/tests/package-version/driver.cxx @@ -12,7 +12,7 @@ #include // operator<<(ostream, exception) #include -#include +#include using namespace std; using namespace butl; diff --git a/tests/repository-location/buildfile b/tests/repository-location/buildfile index 54811d3..ecd7da2 100644 --- a/tests/repository-location/buildfile +++ b/tests/repository-location/buildfile @@ -4,6 +4,6 @@ import libs += libbutl%lib{butl} -exe{driver}: cxx{driver} ../../bpkg/lib{bpkg} $libs +exe{driver}: cxx{driver} ../../libbpkg/lib{bpkg} $libs -include ../../bpkg/ +include ../../libbpkg/ diff --git a/tests/repository-location/driver.cxx b/tests/repository-location/driver.cxx index 953193a..3122393 100644 --- a/tests/repository-location/driver.cxx +++ b/tests/repository-location/driver.cxx @@ -13,7 +13,7 @@ #include #include -#include +#include using namespace std; using namespace butl; -- cgit v1.1