diff options
Diffstat (limited to 'bpkg')
-rw-r--r-- | bpkg/manifest | 46 | ||||
-rw-r--r-- | bpkg/manifest.cxx | 486 |
2 files changed, 343 insertions, 189 deletions
diff --git a/bpkg/manifest b/bpkg/manifest index d443c49..6bfe8e7 100644 --- a/bpkg/manifest +++ b/bpkg/manifest @@ -10,7 +10,7 @@ #include <cassert> #include <cstdint> // uint16_t #include <ostream> -#include <algorithm> // move() +#include <utility> // move() #include <stdexcept> // logic_error #include <butl/path> @@ -363,7 +363,7 @@ namespace bpkg repository_location () = default; // If the argument is not empty, create remote/absolute repository - // location. Throw invalid_argument if the location is a relative + // location. Throw std::invalid_argument if the location is a relative // path. If the argument is empty, then create the special empty // location. // @@ -372,7 +372,7 @@ namespace bpkg // Create a potentially relative repository location. If base is not // empty, use it to complete the relative location to remote/absolute. - // Throw invalid_argument if base is not empty but the location is + // 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. // @@ -458,13 +458,15 @@ namespace bpkg return port_; } - bool - secure () const + enum class protocol {http, https}; + + protocol + proto () const { if (local ()) throw std::logic_error ("local location"); - return secure_; + return proto_; } // Note that this is not necessarily syntactically the same string @@ -477,10 +479,10 @@ namespace bpkg private: std::string canonical_name_; + protocol proto_; std::string host_; std::uint16_t port_; butl::dir_path path_; - bool secure_; }; inline std::ostream& @@ -509,15 +511,6 @@ namespace bpkg butl::optional<std::string> summary; butl::optional<std::string> description; - public: - repository_manifest (manifest_parser&, bool ignore_unknown = false); - repository_manifest (manifest_parser&, - manifest_name_value start, - bool ignore_unknown = false); - - void - serialize (manifest_serializer&) const; - // 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 @@ -525,10 +518,29 @@ namespace bpkg // 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 complete if not) and return that. Otherwise, - // throw logic_error. + // 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<std::string> + effective_url (const repository_location&) const; + + public: + repository_manifest (manifest_parser&, bool ignore_unknown = false); + repository_manifest (manifest_parser&, + manifest_name_value start, + bool ignore_unknown = false); + + void + serialize (manifest_serializer&) const; }; class repository_manifests: public std::vector<repository_manifest> diff --git a/bpkg/manifest.cxx b/bpkg/manifest.cxx index 7f847a3..b7b0aaf 100644 --- a/bpkg/manifest.cxx +++ b/bpkg/manifest.cxx @@ -1188,8 +1188,234 @@ namespace bpkg 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<url_parts::protocol> + is_url (const string& location) + { + using protocol = url_parts::protocol; + + optional<protocol> p; + if (::strncasecmp (location.c_str (), "http://", 7) == 0) + p = protocol::http; + else if (::strncasecmp (location.c_str (), "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<protocol> 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), + 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) + 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<uint16_t> (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. // @@ -1221,135 +1447,22 @@ namespace bpkg if (!b.empty () && b.relative ()) throw invalid_argument ("base relative filesystem path"); - secure_ = false; - - if (::strncasecmp (l.c_str (), "http://", 7) == 0 || - (secure_ = ::strncasecmp (l.c_str (), "https://", 8) == 0)) + if (is_url (l)) { - // Split location into host, port and path components. Calculate - // canonical name <host> part removing www. and pkg. prefixes. - // - size_t host_offset (secure_ ? 8 : 7); - auto p (l.find ('/', host_offset)); - - // The remote repository location with no path specified is not a valid - // one. Keep the path_ member empty so the later check for emptiness - // will throw invalid_argument exception. - // - 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 (l, p + 1, string::npos); + url_parts u (l); + proto_ = u.proto; + host_ = move (u.host); + port_ = u.port; + path_ = move (u.path); - // Put the lower-cased version of the host part into host_. - // Chances are good it will stay unmodified. - // - transform (l.cbegin () + host_offset, - p == string::npos ? l.cend () : l.cbegin () + p, - back_inserter (host_), - lowercase); + canonical_name_ = strip_domain (host_); - // 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. + // 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. // - 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<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". - // - bool bpkg (false); - if (host_.compare (0, 4, "www.") == 0 || - host_.compare (0, 4, "pkg.") == 0 || - (bpkg = host_.compare (0, 5, "bpkg.") == 0)) - canonical_name_.assign (host_, bpkg ? 5 : 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. The same rule apply the HTTPS protocol - // and the port 443. - // - if (port_ != 0 && port_ != (secure_ ? 443 : 80)) - canonical_name_ += ':' + to_string (port_); + if (port_ != 0 && port_ != (proto_ == protocol::http ? 80 : 443)) + canonical_name_ += ':' + std::to_string (port_); } else { @@ -1361,10 +1474,10 @@ namespace bpkg { // Convert the relative path location to an absolute or remote one. // + proto_ = b.proto_; host_ = b.host_; port_ = b.port_; path_ = b.path_ / path_; - secure_ = b.secure_; // Set canonical name to the base location canonical name host // part. The path part of the canonical name is calculated below. @@ -1412,49 +1525,18 @@ namespace bpkg return; } - // Search for the version path component preceeding canonical name - // <path> component. + // Canonical name <prefix>/<path> part. // - 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 p (rb, i); // Canonical name path part. - - // Prefix ends with "pkg" component. - // - bool pc (++i != re && (*i == "pkg" || *i == "bpkg")); - - if (pc) - ++i; // Skip "pkg" component from prefix. - - if (!host_.empty () || !pc) - p = dir_path (i, re) / p; // Concatenate prefix and path. + string cp ( + strip_path (path_, remote () ? strip_mode::component : strip_mode::path). + posix_string ()); // Note: allow empty paths (e.g., http://stable.cppget.org/1/). // - string d (p.posix_string ()); - - if (!canonical_name_.empty () && !d.empty ()) // If we have host and dir. + if (!canonical_name_.empty () && !cp.empty ()) // If we have host and dir. canonical_name_ += '/'; - canonical_name_ += d; + canonical_name_ += cp; // But don't allow empty canonical names. // @@ -1465,20 +1547,13 @@ namespace bpkg string repository_location:: string () const { - using std::string; // Also function name. - if (empty ()) - return string (); + return std::string (); // Also function name. if (local ()) return path_.string (); - string p ((secure_ ? "https://" : "http://") + host_); - - if (port_ != 0) - p += ":" + to_string (port_); - - return p + "/" + path_.posix_string (); + return to_string (proto_, host_, port_, path_); } // repository_manifest @@ -1693,6 +1768,73 @@ namespace bpkg : repository_role::prerequisite; } + optional<string> 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 (); + } + 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:: |