diff options
-rw-r--r-- | bpkg/manifest | 95 | ||||
-rw-r--r-- | bpkg/manifest.cxx | 252 | ||||
-rw-r--r-- | tests/buildfile | 2 | ||||
-rw-r--r-- | tests/repository-location/buildfile | 9 | ||||
-rw-r--r-- | tests/repository-location/driver.cxx | 153 |
5 files changed, 481 insertions, 30 deletions
diff --git a/bpkg/manifest b/bpkg/manifest index a4bf6f2..e8c2714 100644 --- a/bpkg/manifest +++ b/bpkg/manifest @@ -7,9 +7,11 @@ #include <string> #include <vector> -#include <cstdint> // uint16 +#include <cstdint> // uint16_t #include <algorithm> // move() +#include <stdexcept> // logic_error +#include <butl/path> #include <butl/optional> namespace bpkg @@ -20,8 +22,9 @@ namespace bpkg using strings = std::vector<std::string>; - struct version + class version { + public: // Create a special empty version. // version (): epoch_ (0), revision_ (0) {} @@ -145,8 +148,9 @@ namespace bpkg // description // description-file // - struct description: std::string + class description: public std::string { + public: bool file; std::string comment; @@ -164,8 +168,9 @@ namespace bpkg // license // - struct licenses: strings + class licenses: public strings { + public: std::string comment; explicit @@ -175,8 +180,9 @@ namespace bpkg // change // change-file // - struct change: std::string + class change: public std::string { + public: bool file; std::string comment; @@ -194,8 +200,9 @@ namespace bpkg // url // package-url // - struct url: std::string + class url: public std::string { + public: std::string comment; explicit @@ -206,8 +213,9 @@ namespace bpkg // email // package-email // - struct email: std::string + class email: public std::string { + public: std::string comment; explicit @@ -231,8 +239,9 @@ namespace bpkg butl::optional<version_comparison> version; }; - struct dependency_alternatives: std::vector<dependency> + class dependency_alternatives: public std::vector<dependency> { + public: bool conditional; std::string comment; @@ -243,8 +252,9 @@ namespace bpkg // requires // - struct requirement_alternatives: strings + class requirement_alternatives: public strings { + public: bool conditional; std::string comment; @@ -287,17 +297,76 @@ namespace bpkg class repository_location { - // @@ Move all the location verification/canonical name calculation - // here. + public: + // Create a special empty repository_location. // + repository_location () = default; + + explicit + repository_location (const std::string&); + + const std::string& + canonical_name () const noexcept {return canonical_name_;} + + bool + empty () const noexcept {return canonical_name_.empty ();} + + bool + local () const + { + if (empty ()) + throw std::logic_error ("empty location"); + + return host_.empty (); + } - // ... + 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_; + } + + // Note that this is not necessarily syntactically the same + // string as what was used to initialize this location. But + // it should be semantically equivalent. + // + std::string + string () const; + + private: + std::string canonical_name_; + std::string host_; + std::uint16_t port_; + butl::dir_path path_; }; class repository_manifest { public: - std::string location; + repository_location location; public: repository_manifest (manifest_parser&); diff --git a/bpkg/manifest.cxx b/bpkg/manifest.cxx index 2576bea..44a5eff 100644 --- a/bpkg/manifest.cxx +++ b/bpkg/manifest.cxx @@ -4,19 +4,26 @@ #include <bpkg/manifest> +#include <strings.h> // strncasecmp() + #include <string> #include <ostream> #include <sstream> #include <cassert> #include <cstring> // strncmp() #include <utility> // move() -#include <algorithm> // find() +#include <cstdint> // uint64_t, uint16_t, UINT16_MAX +#include <iterator> // back_insert_iterator +#include <algorithm> // find(), transform() #include <stdexcept> // invalid_argument +#include <butl/path> + #include <bpkg/manifest-parser> #include <bpkg/manifest-serializer> using namespace std; +using namespace butl; namespace bpkg { @@ -32,23 +39,32 @@ namespace bpkg static const string spaces (" \t"); inline static bool - space (char c) + space (char c) noexcept { return c == ' ' || c == '\t'; } inline static bool - digit (char c) + digit (char c) noexcept { return c >= '0' && c <= '9'; } inline static bool - alpha (char c) + alpha (char c) noexcept { return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; } + // Replace std::tolower to keep things locale independent. + // + inline static char + lowercase (char c) noexcept + { + const unsigned char shift ('a' - 'A'); + return c >= 'A' && c <='Z' ? c + shift : c; + } + static ostream& operator<< (ostream& o, const dependency& d) { @@ -175,7 +191,9 @@ namespace bpkg version:: version (const char* v, bool upstream_only): version () // Delegate { - using std::string; // Otherwise compiler get confused with string() member. + // Otherwise compiler gets confused with string() member. + // + using std::string; assert (v != nullptr); @@ -211,16 +229,8 @@ namespace bpkg } else { - // Don't use tolower to keep things locale independent. - // - static unsigned char shift ('a' - 'A'); - for (const char* i (b); i != e; ++i) - { - char c (*i); - canonical_upstream_.append ( - 1, c >= 'A' && c <='Z' ? c + shift : c); - } + canonical_upstream_.append (1, lowercase (*i)); return true; } @@ -736,6 +746,204 @@ namespace bpkg s.next ("", ""); // End of manifest. } + // repository_location + // + repository_location:: + repository_location (const std::string& l): port_ (0) + { + // Otherwise compiler gets confused with string() member. Same reason + // constructor parameter type is fully qualified. + // + using std::string; + + if (::strncasecmp (l.c_str (), "http://", 7) == 0) + { + // Split location into host, port and path components. Calculate + // canonical name <host> part removing www. and pkg. prefixes. + // + auto p (l.find ('/', 7)); + + // Chop the path part. Note that we translate empty path to "/". + // + path_ = p != string::npos + ? dir_path (l, p, string::npos) + : dir_path ("/"); + + // Put the lower-cased version of the host part into host_. + // Chances are good it will stay unmodified. + // + transform (l.cbegin () + 7, + p == string::npos ? l.cend () : l.cbegin () + p, + back_inserter (host_), + lowercase); + + // 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) + { + 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<uint16_t> (n); + host_.resize (pt - hb - 1); + } + + if (host_.empty ()) + throw invalid_argument ("invalid host"); + + // Ok, the last thing we need to do is add the host and port + // parts to the canonical_name_ name. Here we also need to + // chop off the special "www" and "pkg" prefixes. Strictly + // speaking we can end up with comething bogus like "com" + // if the host is "pkg.com". + // + if (host_.compare (0, 4, "www.") == 0 || + host_.compare (0, 4, "pkg.") == 0) + canonical_name_.assign (host_, 4, string::npos); + else + canonical_name_ = host_; + + // For canonical name and for the HTTP protocol, treat a.com + // and a.com:80 as the same name. + // + if (port_ != 0 && port_ != 80) + canonical_name_ += ':' + to_string (port_); + } + else + path_ = dir_path (l); + + // 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"); + } + + // Search for the version path component preceeding canonical name + // <path> component. + // + auto b (path_.rbegin ()), i (b), e (path_.rend ()); + + // Find the version component. + // + for (; i != e; ++i) + { + const string& c (*i); + + if (!c.empty () && c.find_first_not_of ("1234567890") == string::npos) + break; + } + + if (i == e) + 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"); + + // Note: allow empty paths (e.g., http://stable.cppget.org/1/). + // + string d (dir_path (b, i).posix_string ()); + + if (!canonical_name_.empty () && !d.empty ()) // If we have host and dir. + canonical_name_ += '/'; + + canonical_name_ += d; + + // 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 ""; + + if (local ()) + return path_.string (); + + std::string p ("http://" + host_); + + if (port_ != 0) + p += ":" + to_string (port_); + + return p + path_.posix_string (); + } + // repository_manifest // repository_manifest:: @@ -773,7 +981,19 @@ namespace bpkg string& v (nv.value); if (n == "location") - location = move (v); + { + if (!v.empty ()) + { + try + { + location = repository_location (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (e.what ()); + } + } + } else bad_name ("unknown name '" + n + "' in repository manifest"); } @@ -789,7 +1009,7 @@ namespace bpkg s.next ("", "1"); // Start of manifest. if (!location.empty ()) - s.next ("location", location); + s.next ("location", location.string ()); s.next ("", ""); // End of manifest. } diff --git a/tests/buildfile b/tests/buildfile index 7162cc7..bb53854 100644 --- a/tests/buildfile +++ b/tests/buildfile @@ -3,7 +3,7 @@ # license : MIT; see accompanying LICENSE file d = manifest-parser/ manifest-serializer/ manifest-roundtrip/ manifest/ \ - package-version/ + package-version/ repository-location/ .: $d include $d diff --git a/tests/repository-location/buildfile b/tests/repository-location/buildfile new file mode 100644 index 0000000..24c6335 --- /dev/null +++ b/tests/repository-location/buildfile @@ -0,0 +1,9 @@ +# file : tests/repository-location/buildfile +# copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +exe{driver}: cxx{driver} ../../bpkg/lib{bpkg} + +include ../../bpkg/ + +# test: ./driver diff --git a/tests/repository-location/driver.cxx b/tests/repository-location/driver.cxx new file mode 100644 index 0000000..be51eb0 --- /dev/null +++ b/tests/repository-location/driver.cxx @@ -0,0 +1,153 @@ +// file : tests/repository-location/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <string> +#include <cassert> +#include <iostream> +#include <exception> +#include <stdexcept> // invalid_argument + +#include <bpkg/manifest> + +using namespace std; +using namespace bpkg; + +static bool +bad_location (const string& l) +{ + try + { + repository_location bl (l); + return false; + } + catch (const invalid_argument&) + { + return true; + } +} + +int +main (int argc, char* argv[]) +{ + if (argc != 1) + { + cerr << "usage: " << argv[0] << endl; + return 1; + } + + try + { + // Test invalid locations. + // + + // Invalid host. + // + assert (bad_location ("http:///aa/bb")); + assert (bad_location ("http://1/aa/bb")); + assert (bad_location ("http:///1/aa/bb")); + assert (bad_location ("http://1a/aa/bb")); + assert (bad_location ("http://a..a/aa/bb")); + assert (bad_location ("http://.a.a/aa/bb")); + assert (bad_location ("http://a.a./aa/bb")); + assert (bad_location ("http://a.1a/aa/bb")); + assert (bad_location ("http://a.1a.a/aa/bb")); + assert (bad_location ("http://a.-ab/aa/bb")); + assert (bad_location ("http://a.-ab.a/aa/bb")); + assert (bad_location ("http://a.ab-/aa/bb")); + assert (bad_location ("http://a.ab-.a/aa/bb")); + assert (bad_location ("http://a.ab-:80/aa/bb")); + assert (bad_location ("http://a.ab.:80/aa/bb")); + assert (bad_location ("http://1.1.1.1.r/1/b")); + assert (bad_location ("http://www./aa/1/bb")); + + assert (bad_location ("http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa.org/1/b/")); + + // Invalid port. + // + assert (bad_location ("http://a:/aa/bb")); + assert (bad_location ("http://a:1b/aa/bb")); + assert (bad_location ("http://c.ru:8a80/1/b")); + assert (bad_location ("http://c.ru:8:80/1/b")); + assert (bad_location ("http://a:0/aa/bb")); + assert (bad_location ("http://c.ru:65536/1/b")); + + // Invalid path. + // + assert (bad_location ("")); + assert (bad_location ("1")); + assert (bad_location ("1/")); + assert (bad_location ("bbb")); + assert (bad_location ("aaa/bbb")); + assert (bad_location ("/aaa/bbb")); + assert (bad_location ("http://aa/bb")); + assert (bad_location ("http://a.com/../c/1/aa")); + + // Invalid version. + // + assert (bad_location ("3/aaa/bbb")); + + // Test valid locations. + // + { + repository_location l ("1/aa/bb"); + assert (l.string () == "1/aa/bb"); + assert (l.canonical_name () == "aa/bb"); + } + { + repository_location l ("/a/b/../c/1/aa/../bb"); + assert (l.string () == "/a/c/1/bb"); + assert (l.canonical_name () == "bb"); + } + { + repository_location l ("../c/../c/./1/aa/../bb"); + assert (l.string () == "../c/1/bb"); + assert (l.canonical_name () == "bb"); + } + { + repository_location l ("http://www.a.com:80/1/aa/bb"); + assert (l.string () == "http://www.a.com:80/1/aa/bb"); + assert (l.canonical_name () == "a.com/aa/bb"); + } + { + repository_location l ("http://www.a.com:8080/dd/1/aa/bb"); + assert (l.string () == "http://www.a.com:8080/dd/1/aa/bb"); + assert (l.canonical_name () == "a.com:8080/aa/bb"); + } + { + repository_location l ("http://a.com/a/b/../c/1/aa/../bb"); + assert (l.string () == "http://a.com/a/c/1/bb"); + assert (l.canonical_name () == "a.com/bb"); + } + { + repository_location l ("http://www.CPPget.org/qw/1/a/b/"); + assert (l.string () == "http://www.cppget.org/qw/1/a/b"); + assert (l.canonical_name () == "cppget.org/a/b"); + } + { + repository_location l ("http://abc.cppget.org/qw/1/a/b/"); + assert (l.string () == "http://abc.cppget.org/qw/1/a/b"); + assert (l.canonical_name () == "abc.cppget.org/a/b"); + } + { + repository_location l ("http://pkg.www.cppget.org/qw/1/a/b/"); + assert (l.string () == "http://pkg.www.cppget.org/qw/1/a/b"); + assert (l.canonical_name () == "www.cppget.org/a/b"); + } + { + repository_location l ("http://cppget.org/qw//1/a//b/"); + assert (l.string () == "http://cppget.org/qw/1/a/b"); + assert (l.canonical_name () == "cppget.org/a/b"); + } + { + repository_location l ("http://stable.cppget.org/1/"); + assert (l.canonical_name () == "stable.cppget.org"); + } + } + catch (const exception& e) + { + cerr << e.what () << endl; + return 1; + } +} |