From 87392531922fa91f05672eb4806735745b195588 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 21 Dec 2017 18:16:16 +0300 Subject: Add support for git repository locations --- libbpkg/manifest.cxx | 618 +++++++++++++++++++++++++++++++-------------------- libbpkg/manifest.hxx | 201 +++++++++++++---- 2 files changed, 542 insertions(+), 277 deletions(-) (limited to 'libbpkg') diff --git a/libbpkg/manifest.cxx b/libbpkg/manifest.cxx index e8034f3..b38f85b 100644 --- a/libbpkg/manifest.cxx +++ b/libbpkg/manifest.cxx @@ -10,9 +10,8 @@ #include #include // strncmp(), strcmp() #include // move() -#include // uint64_t, uint16_t, UINT16_MAX -#include // back_insert_iterator -#include // find(), transform() +#include // uint16_t, UINT16_MAX +#include // find(), replace() #include // invalid_argument #include @@ -447,6 +446,8 @@ namespace bpkg string version:: string (bool ignore_revision) const { + using std::to_string; // Hidden by to_string (repository_type). + if (empty ()) throw logic_error ("empty version"); @@ -1410,200 +1411,266 @@ namespace bpkg s.next ("", ""); // End of stream. } - // url_parts + // repository_url_traits // - struct url_parts + repository_url_traits::scheme_type repository_url_traits:: + translate_scheme (const string_type& url, + string_type&& scheme, + optional& authority, + optional& path, + optional& query, + optional& fragment) { - 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; + auto bad_url = [] (const char* d = "invalid URL") + { + throw invalid_argument (d); + }; - string::size_type host_offset (s.find ("//")); - assert (host_offset != string::npos); - host_offset += 2; + auto translate_remote = [&authority, &path, &query, &fragment, &bad_url] () + { + if (!authority || authority->host.empty ()) + bad_url ("invalid host"); - string::size_type p (s.find ('/', host_offset)); + if (authority->host.kind != url_host_kind::name) + bad_url ("unsupported host type"); - 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. + // Normalize the host name. // - path = dir_path (s, p + 1, string::npos); + lcase (authority->host.value); - // 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)); + // We don't distinguish between the absent and empty paths for the + // remote repository URLs. + // + if (!path) + path = path_type (); - // 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. + if (path->absolute ()) + bad_url ("absolute path"); + }; - for (auto i (hb); i != he; ++i) + if (casecmp (scheme, "http") == 0) { - char c (*i); - - if (pt == he) // Didn't reach port specification yet. + translate_remote (); + return scheme_type::http; + } + else if (casecmp (scheme, "https") == 0) + { + translate_remote (); + return scheme_type::https; + } + else if (casecmp (scheme, "git") == 0) + { + translate_remote (); + return scheme_type::git; + } + else if (casecmp (scheme, "file") == 0) + { + if (authority) { - if (c == ':') // Port specification reached. - pt = i; - else - { - auto n (i + 1); + if (!authority->empty () && + (casecmp (authority->host, "localhost") != 0 || + authority->port != 0 || + !authority->user.empty ())) + throw invalid_argument ("invalid authority"); + + // We don't distinguish between the absent, empty and localhost + // authorities for the local repository locations. + // + authority = nullopt; + } - // Validate host name. - // + if (!path) + bad_url ("absent path"); - // Is first label char. - // - bool flc (i == ls); + // On POSIX make the relative (to the authority "root") path absolute. On + // Windows it must already be an absolute path. + // +#ifndef _WIN32 + if (path->absolute ()) + bad_url ("absolute path"); - // Is last label char. - // - bool llc (n == he || *n == '.' || *n == ':'); + path = path_type ("/") / *path; +#else + if (path->relative ()) + bad_url ("relative path"); +#endif - // Validate char. - // - bool valid (alpha (c) || - (digit (c) && !flc) || - ((c == '-' || c == '.') && !flc && !llc)); + if (query) + bad_url (); - // Validate length. - // - if (valid) - valid = i - ls < 64 && i - hb < 256; + return scheme_type::file; + } + // Consider URL as a path if the URL parsing failed. + // + else if (scheme.empty ()) + { + try + { + path = path_type (url); + } + catch (const invalid_path&) + { + // If this is not a valid path either, then let's consider the argument + // a broken URL, and leave the basic_url ctor to throw. + // + } - if (!valid) - throw invalid_argument ("invalid host"); + return scheme_type::file; + } + else + throw invalid_argument ("unknown scheme"); + } - if (c == '.') - ls = n; - } - } - else + repository_url_traits::string_type repository_url_traits:: + translate_scheme (string_type& url, + const scheme_type& scheme, + const optional& /*authority*/, + const optional& path, + const optional& /*query*/, + const optional& fragment) + { + switch (scheme) + { + case scheme_type::http: return "http"; + case scheme_type::https: return "https"; + case scheme_type::git: return "git"; + case scheme_type::file: { - // Validate port. + // If there is no fragment present then represent the URL object as a + // local path. // - if (!digit (c)) - throw invalid_argument ("invalid port"); + assert (path); + + if (fragment) + { + assert (path->absolute ()); + return "file"; + } + + url = path->relative () ? path->posix_string () : path->string (); + return string_type (); } } - // Chop the port, if present. - // - if (pt == he) - port = 0; - else + assert (false); // Can't be here. + return ""; + } + + repository_url_traits::path_type repository_url_traits:: + translate_path (string_type&& path) + { + try + { + return path_type (move (path)); + } + catch (const invalid_path&) { - unsigned long long n (++pt == he ? 0 : stoull (string (pt, he))); - if (n == 0 || n > UINT16_MAX) - throw invalid_argument ("invalid port"); + throw invalid_argument ("invalid url"); + } + } - port = static_cast (n); - host.resize (pt - hb - 1); + repository_url_traits::string_type repository_url_traits:: + translate_path (const path_type& path) + { + // If the path is absolute then this is a local URL object and the file:// + // URL notation is being produced. Thus, on POSIX we need to make the path + // relative (to the authority "root"). On Windows the path should stay + // absolute but the directory separators must be converted to the POSIX + // ones. + // + if (path.absolute ()) + { +#ifndef _WIN32 + return path.leaf (dir_path ("/")).string (); +#else + string r (path.string ()); + replace (r.begin (), r.end (), '\\', '/'); + return r; +#endif + } + + return path.posix_string (); + } + + // repository_type + // + string + to_string (repository_type t) + { + switch (t) + { + case repository_type::bpkg: return "bpkg"; + case repository_type::git: return "git"; } - if (host.empty ()) - throw invalid_argument ("invalid host"); + assert (false); // Can't be here. + return string (); + } + + repository_type + to_repository_type (const string& t) + { + if (t == "bpkg") return repository_type::bpkg; + else if (t == "git") return repository_type::git; + else throw invalid_argument ("invalid repository type '" + t + "'"); } // repository_location // static string - strip_domain (const string& host) + strip_domain (const string& host, repository_type type) { assert (!host.empty ()); // Should be repository location host. - string h; - bool bpkg (false); + optional h; - if (host.compare (0, 4, "www.") == 0 || - host.compare (0, 4, "pkg.") == 0 || - (bpkg = host.compare (0, 5, "bpkg.") == 0)) + switch (type) { - if (h.assign (host, bpkg ? 5 : 4, string::npos).empty ()) - throw invalid_argument ("invalid host"); + case repository_type::bpkg: + { + bool bpkg (false); + if (host.compare (0, 4, "www.") == 0 || + host.compare (0, 4, "pkg.") == 0 || + (bpkg = host.compare (0, 5, "bpkg.") == 0)) + h = string (host, bpkg ? 5 : 4); + + break; + } + case repository_type::git: + { + if (host.compare (0, 4, "www.") == 0 || + host.compare (0, 4, "git.") == 0 || + host.compare (0, 4, "scm.") == 0) + h = string (host, 4); + + break; + } } - else - h = host; - return h; + if (h && h->empty ()) + throw invalid_argument ("invalid host"); + + return h ? *h : host; } - // The 'pkg' path component stripping mode. + // The 'pkg' path component and '.git' extension stripping mode. // - enum class strip_mode {version, component, path}; + enum class strip_mode {version, component, path, extension}; - static dir_path - strip_path (const dir_path& path, strip_mode mode) + static path + strip_path (const path& p, strip_mode mode) { - // Should be repository location path. + if (mode == strip_mode::extension) + { + const char* e (p.extension_cstring ()); + return e != nullptr && strcmp (e, "git") == 0 ? p.base () : p; + } + + // Should be bpkg repository location path. // - assert (!path.empty () && *path.begin () != ".."); + assert (!p.empty () && *p.begin () != ".."); - auto rb (path.rbegin ()), i (rb), re (path.rend ()); + auto rb (p.rbegin ()), i (rb), re (p.rend ()); // Find the version component. // @@ -1623,7 +1690,7 @@ namespace bpkg if (stoul (*i) != 1) throw invalid_argument ("unsupported repository version"); - dir_path res (rb, i); + path res (rb, i); // Canonical name prefix part ends with the special "pkg" component. // @@ -1633,30 +1700,31 @@ namespace bpkg ++i; // Strip the "pkg" component. if (!pc || mode != strip_mode::path) - res = dir_path (i, re) / res; // Concatenate prefix and path parts. + res = 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. + repository_location (repository_url u, repository_type t) + : repository_location (move (u), t, repository_location ()) // Delegate. { if (!empty () && relative ()) throw invalid_argument ("relative filesystem path"); } repository_location:: - repository_location (const std::string& l, const repository_location& b) + repository_location (repository_url u, + repository_type t, + const repository_location& b) + : url_ (move (u)), + type_ (t) { - // Otherwise compiler gets confused with string() member. - // - using std::string; + using std::string; // Hidden by string(). + using std::to_string; // Hidden by to_string(repository_type). + using butl::path; // Hidden by path(). - if (l.empty ()) + if (url_.empty ()) { if (!b.empty ()) throw invalid_argument ("empty location"); @@ -1664,42 +1732,100 @@ namespace bpkg return; } - // Base repository location can not be a relative path. + // Make sure that the URL object is properly constructed (see notes for the + // repository_url class in the header). // - if (!b.empty () && b.relative ()) - throw invalid_argument ("base relative filesystem path"); + assert (url_.path && + remote () == (url_.authority && !url_.authority->empty ())); - if (is_url (l)) + // Verify that the URL object matches the repository type. + // + switch (t) { - url_parts u (l); - proto_ = u.proto; - host_ = move (u.host); - port_ = u.port; - path_ = move (u.path); + case repository_type::bpkg: + { + if (url_.scheme == repository_protocol::git) + throw invalid_argument ("unsupported scheme for bpkg repository"); + + break; + } + case repository_type::git: + { + if (!url_.fragment) + throw invalid_argument ("missing branch/tag for git repository"); + + break; + } + } + + // Base repository location is only meaningful for bpkg repository, and + // can not be a relative path. + // + if (!b.empty ()) + { + if (type_ != repository_type::bpkg) + throw invalid_argument ("unexpected base location"); + + if (b.type () != repository_type::bpkg) + throw invalid_argument ("invalid base location"); + + if (b.relative ()) + throw invalid_argument ("base location is relative filesystem path"); + } + + path& up (*url_.path); + + // For the repositories that are "directories", make sure that the URL + // object's path is a directory (contains the trailing slash). + // + switch (t) + { + case repository_type::bpkg: + case repository_type::git: + { + if (!up.to_directory ()) + up = path_cast (move (up)); + break; + } + } - canonical_name_ = strip_domain (host_); + if (remote ()) + { + canonical_name_ = strip_domain (url_.authority->host, type_); // 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. + // a.com:80 as the same name. The same rule applies to the HTTPS (port + // 443) and GIT (port 9418) protocols. // - if (port_ != 0 && port_ != (proto_ == protocol::http ? 80 : 443)) - canonical_name_ += ':' + std::to_string (port_); + uint16_t port (url_.authority->port); + if (port != 0) + { + uint16_t def_port; + + switch (url_.scheme) + { + case repository_protocol::http: def_port = 80; break; + case repository_protocol::https: def_port = 443; break; + case repository_protocol::git: def_port = 9418; break; + case repository_protocol::file: assert (false); // Can't be here. + } + + if (port != def_port) + canonical_name_ += ':' + to_string (port); + } } else { - path_ = dir_path (l); - // Complete if we are relative and have base. // - if (!b.empty () && path_.relative ()) + if (!b.empty () && up.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_; + repository_url u (b.url ()); + *u.path /= up; + + url_ = move (u); // Set canonical name to the base location canonical name host // part. The path part of the canonical name is calculated below. @@ -1716,26 +1842,27 @@ namespace bpkg // try { - path_.normalize (); + up.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). + // For bpkg repository we need to check path for emptiness before + // proceeding further as a valid non empty location can not have an empty + // URL object path member (which can be the case for the remote location, + // but not for the relative or absolute). // - if (path_.empty ()) + if (type_ == repository_type::bpkg && up.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. + // this check as soon as URL object path member contains a relative + // directory for the remote location. // - if (remote () && *path_.begin () == "..") + if (remote () && !up.empty () && *up.begin () == "..") throw invalid_argument ("invalid path"); // Finish calculating the canonical name, unless we are relative. @@ -1746,19 +1873,29 @@ namespace bpkg return; } - // Canonical name / part. + // Canonical name / part for the bpkg repository, and the + // full path with the .git extension stripped for the git repository. // - dir_path sp (strip_path ( - path_, remote () ? strip_mode::component : strip_mode::path)); + path sp; - // 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); + if (type_ == repository_type::bpkg) + { + sp = strip_path (up, + 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 (up, strip_mode::version); + } + else + sp = strip_path (up, strip_mode::extension); - string cp (sp.relative () ? sp.posix_string () : sp.string ()); + string cp (sp.relative () ? sp.posix_string () : sp.string ()); // Note: allow empty paths (e.g., http://stable.cppget.org/1/). // @@ -1773,18 +1910,6 @@ namespace bpkg 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:: @@ -1834,7 +1959,8 @@ namespace bpkg // Call prerequisite repository location constructor, do not // ammend relative path. // - location = repository_location (move (v), repository_location ()); + location = repository_location (repository_url (move (v)), + repository_location ()); } catch (const invalid_argument& e) { @@ -1947,7 +2073,12 @@ namespace bpkg s.next ("", "1"); // Start of manifest. if (!location.empty ()) + { + if (location.remote () && location.type () != repository_type::bpkg) + bad_value ("invalid repository location"); + s.next ("location", location.string ()); + } if (role) { @@ -2023,27 +2154,33 @@ namespace bpkg optional repository_manifest:: effective_url (const repository_location& l) const { + static const char* invalid_location ("invalid repository location"); + + if (l.local () || l.type () != repository_type::bpkg) + throw invalid_argument (invalid_location); + if (!url || (*url)[0] != '.') return url; - const dir_path rp (*url); + const 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; + auto strip = [&i, &rp]() -> bool + { + if (i != rp.end ()) + { + const auto& c (*i++); + if (c == "..") + return true; - if (c == ".") - return false; - } + if (c == ".") + return false; + } - throw invalid_argument (invalid_url); - }); + throw invalid_argument (invalid_url); + }; bool strip_d (strip ()); // Strip domain. bool strip_p (strip ()); // Strip path. @@ -2051,10 +2188,14 @@ namespace bpkg // The web interface relative path with the special first two components // stripped. // - const dir_path rpath (i, rp.end ()); + const path rpath (i, rp.end ()); assert (rpath.relative ()); - url_parts u (l.string ()); + repository_url u (l.url ()); + + if (strip_d) + u.authority->host.value = strip_domain (u.authority->host, + repository_type::bpkg); // Web interface URL path part. // @@ -2063,12 +2204,11 @@ namespace bpkg // 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"); + path ipath (strip_path (*u.path, + strip_p + ? strip_mode::component + : strip_mode::version) / + rpath); try { @@ -2084,8 +2224,10 @@ namespace bpkg 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); + // Strip the trailing slash for an empty path. + // + u.path = !ipath.empty () ? move (ipath) : optional (); + return u.string (); } // repository_manifests @@ -2118,7 +2260,7 @@ namespace bpkg throw serialization (s.name (), "base repository manifest expected"); // @@ Should we check that there is location in all except the last - // entry? + // entry? // for (const repository_manifest& r: *this) r.serialize (s); diff --git a/libbpkg/manifest.hxx b/libbpkg/manifest.hxx index a72986a..7f5bcda 100644 --- a/libbpkg/manifest.hxx +++ b/libbpkg/manifest.hxx @@ -13,6 +13,7 @@ #include // move() #include // logic_error +#include #include #include #include @@ -422,6 +423,95 @@ namespace bpkg serialize (butl::manifest_serializer&) const; }; + // Traits class for the repository URL object. + // + enum class repository_protocol {file, http, https, git}; + + struct LIBBPKG_EXPORT repository_url_traits + { + using string_type = std::string; + using path_type = butl::path; + + using scheme_type = repository_protocol; + using authority_type = butl::basic_url_authority; + + static scheme_type + translate_scheme (const string_type&, + string_type&&, + butl::optional&, + butl::optional&, + butl::optional&, + butl::optional&); + + + static string_type + translate_scheme (string_type&, + const scheme_type&, + const butl::optional&, + const butl::optional&, + const butl::optional&, + const butl::optional&); + + static path_type + translate_path (string_type&&); + + static string_type + translate_path (const path_type&); + }; + + // Repository URL. Note that it represents both the remote (http(s)://, + // git://, etc) and local (file:// as well as plain directory path) + // repository URLs. May also be empty. + // + // Notes: + // + // - For an empty URL object all components are absent. For a non-empty one + // the directory path is always present and normalized. + // + // - For the remote URL object the host name is in the lower case (IPv4/6 are + // not supported) and the path is relative. + // + // - For the local URL object the path can be relative or absolute. Query + // can not be present. Fragment can not be present for the relative path + // as there is no notation that can be used to represent it. + // + struct LIBBPKG_EXPORT repository_url: butl::basic_url + { + using base_type = basic_url; + + using base_type::base_type; + + // Create an empty URL object. + // + repository_url () = default; + + // If the argument is an empty string, then create an empty URL object. If + // the argument is a local path or a file URL then create a local URL + // object. Otherwise create a remote URL object. + // + // May throw std::invalid_argument (unknown URL schema, incorrect syntax, + // unexpected components, etc). + // + repository_url (const string_type& s): base_type (s) {} + }; + + // Repository type. + // + enum class repository_type {bpkg, git}; + + LIBBPKG_EXPORT std::string + to_string (repository_type); + + LIBBPKG_EXPORT repository_type + to_repository_type (const std::string&); // May throw std::invalid_argument. + + inline std::ostream& + operator<< (std::ostream& os, repository_type t) + { + return os << to_string (t); + } + class LIBBPKG_EXPORT repository_location { public: @@ -429,28 +519,29 @@ namespace bpkg // 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. + // Create remote, absolute or empty repository location making sure that + // the URL matches the repository type. Throw std::invalid_argument if the + // URL object is a relative local path. // explicit - repository_location (const std::string&); + repository_location (repository_url, repository_type); - // 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 + // Create a potentially relative bpkg repository location. If base is not + // empty, use it to complete the relative location to the remote/absolute + // one. 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 (repository_url u, + const repository_location& base) + : repository_location (std::move (u), repository_type::bpkg, base) {} repository_location (const repository_location& l, const repository_location& base) - : repository_location (l.string (), base) {} + : repository_location (l.url (), l.type (), base) {} - // Note that relative locations have no canonical name. Canonical - // name of an empty location is the empty name. + // 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_;} @@ -462,7 +553,7 @@ namespace bpkg // other predicates throw std::logic_error for an empty location. // bool - empty () const noexcept {return path_.empty ();} + empty () const noexcept {return url_.empty ();} bool local () const @@ -470,7 +561,7 @@ namespace bpkg if (empty ()) throw std::logic_error ("empty location"); - return host_.empty (); + return url_.scheme == repository_protocol::file; } bool @@ -487,22 +578,42 @@ namespace bpkg // Note that in remote locations path is always relative. // - return path_.absolute (); + return url_.path->absolute (); } bool relative () const { - return local () && path_.relative (); + return local () && url_.path->relative (); + } + + repository_type + type () const + { + if (empty ()) + throw std::logic_error ("empty location"); + + return type_; + } + + // URL of an empty location is empty. + // + repository_url + url () const + { + return url_; } - const butl::dir_path& + // Repository path. Note that for repository types that refer to + // "directories" it always contains the trailing slash. + // + const butl::path& path () const { if (empty ()) throw std::logic_error ("empty location"); - return path_; + return *url_.path; } const std::string& @@ -511,7 +622,7 @@ namespace bpkg if (local ()) throw std::logic_error ("local location"); - return host_; + return url_.authority->host; } // Value 0 indicated that no port was specified explicitly. @@ -522,34 +633,46 @@ namespace bpkg if (local ()) throw std::logic_error ("local location"); - return port_; + return url_.authority->port; } - enum class protocol {http, https}; - - protocol + repository_protocol proto () const { - if (local ()) - throw std::logic_error ("local location"); + if (empty ()) + throw std::logic_error ("empty location"); - return proto_; + return url_.scheme; } - // 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. + const butl::optional& + fragment () const + { + if (relative ()) + throw std::logic_error ("relative filesystem path"); + + return url_.fragment; + } + + // String representation of an empty location is the empty string. // std::string - string () const; + string () const + { + return url_.string (); + } + + private: + // Used for delegating in public constructor. + // + repository_location (repository_url, + repository_type, + const repository_location& base); private: std::string canonical_name_; - protocol proto_; - std::string host_; - std::uint16_t port_; - butl::dir_path path_; + repository_url url_; + repository_type type_; }; inline std::ostream& @@ -570,7 +693,7 @@ namespace bpkg public: using email_type = bpkg::email; - repository_location location; + repository_location location; // bpkg repository location. butl::optional role; // The following values may only be present for the base repository. @@ -594,8 +717,8 @@ namespace bpkg 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 + // bpkg 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. -- cgit v1.1