diff --git a/Migration.md b/Migration.md index b13058da3..69f1bb4bb 100644 --- a/Migration.md +++ b/Migration.md @@ -12,8 +12,8 @@ release will remove the deprecated code. 1. Corrected `BAYER_RGGR8` to `BAYER_BGGR8` in `PixelFormatName` and `PixelFormatType` located in `graphics/include/ignition/common/Image.hh`. -1. URI parsing has updated to follow the specification more closely. Changes - include: +1. URI parsing has updated to follow the specification more closely when + `URI::Authority` is set. Changes include: * An empty URI Path is valid. * Double forward slashes, `//`, are valid in a URI Path. * A URI Query does not require a `key=value` format. For example diff --git a/include/ignition/common/SystemPaths.hh b/include/ignition/common/SystemPaths.hh index e88f482e7..ddd0fcc17 100644 --- a/include/ignition/common/SystemPaths.hh +++ b/include/ignition/common/SystemPaths.hh @@ -71,9 +71,13 @@ namespace ignition /// \param[in] _uri the uniform resource identifier /// \return Returns full path name to file with platform-specific /// directory separators, or an empty string if URI couldn't be found. + /// \sa FindFileURI(const ignition::common::URI &_uri) public: std::string FindFileURI(const std::string &_uri) const; - /// \brief Find a file or path using a URI + /// \brief Find a file or path using a URI. + /// If URI is not an absolute path, the URI's path will be matched against + /// all added paths and environment variables (including URI authority as + /// needed). /// \param[in] _uri the uniform resource identifier /// \return Returns full path name to file with platform-specific /// directory separators, or an empty string if URI couldn't be found. @@ -87,10 +91,12 @@ namespace ignition /// \param[in] _filename Name of the file to find. /// \param[in] _searchLocalPath True to search in the current working /// directory. + /// \param[in] _verbose False to omit console messages. /// \return Returns full path name to file with platform-specific /// directory separators, or empty string on error. public: std::string FindFile(const std::string &_filename, - const bool _searchLocalPath = true) const; + const bool _searchLocalPath = true, + const bool _verbose = true) const; /// \brief Find a shared library by name in the plugin paths /// diff --git a/include/ignition/common/URI.hh b/include/ignition/common/URI.hh index e7371e31f..f4dba47b4 100644 --- a/include/ignition/common/URI.hh +++ b/include/ignition/common/URI.hh @@ -52,6 +52,13 @@ namespace ignition /// authority. For example, `file://abs/path` will result in a host /// value of `abs` and the URI path will be `/path`. You can specify /// a relative path using `file:abs/path`. + /// + /// URIs that are set not to have an authority + /// (i.e. `Authority() == false`) preserve the legacy behaviour, which + /// is: + /// * `file:/abs/path` has the path `/abs/path` + /// * `file://abs/path` has the path `abs/path` + /// * `file:///abs/path` has the path `/abs/path` class IGNITION_COMMON_VISIBLE URIAuthority { /// \brief Constructor @@ -148,7 +155,15 @@ namespace ignition IGN_COMMON_WARN_RESUME__DLL_INTERFACE_MISSING }; - /// \brief The path component of a URI + /// \brief A URI path contains a sequence of segments separated by `/`. + /// The path may be empty in a valid URI. + /// When an authority is present, the path must start with a `/`. + /// + /// In the following URI: + /// + /// scheme://authority.com/seg1/seg2?query + /// + /// The path is `/seg1/seg2` class IGNITION_COMMON_VISIBLE URIPath { /// \brief Constructor @@ -203,6 +218,16 @@ namespace ignition /// and a Windows drive specifier (e.g. 'C:') is pushed to it. public: void PushBack(const std::string &_part); + /// \brief Remove the part that's in the front of this path and return it. + /// \return Popped part. + /// Returns empty string if path doesn't have parts to be popped. + public: std::string PopFront(); + + /// \brief Remove the part that's in the back of this path and return it. + /// \return Popped part. + /// Returns empty string if path doesn't have parts to be popped. + public: std::string PopBack(); + /// \brief Compound assignment operator. /// \param[in] _part A new path to append. /// \return A new Path that consists of "this / _part" @@ -383,9 +408,12 @@ namespace ignition /// \brief Default constructor public: URI(); - /// \brief Construct a URI object from a string. - /// \param[in] _str A string. - public: explicit URI(const std::string &_str); + /// \brief Default constructor + /// \param[in] _hasAuthority False if the URI doesn't have an authority. + /// Defaults to false. If true, an authority will be created and will be + /// empty. + public: explicit URI(const std::string &_str, + bool _hasAuthority = false); /// \brief Copy constructor /// \param[in] _uri Another URI. @@ -412,13 +440,16 @@ namespace ignition /// \param[in] _scheme New scheme. public: void SetScheme(const std::string &_scheme); - /// \brief Get a mutable version of the URI's authority. + /// \brief Set the URI's authority. /// \return The authority - public: URIAuthority &Authority(); + public: void SetAuthority(const URIAuthority &_authority); - /// \brief Get a const reference of the URI's authority. + /// \brief Get a copy of the URI's authority. + /// If the authority has no value (as opposed to being empty), it means + /// that it isn't present, and the authority value may be contained in + /// the path instead. /// \return The authority - public: const URIAuthority &Authority() const; + public: std::optional Authority() const; /// \brief Get a mutable version of the path component /// \return A reference to the path @@ -464,6 +495,10 @@ namespace ignition public: static bool Valid(const std::string &_str); /// \brief Parse a string as URI. + /// If there's no authority (i.e. `Authority().has_value() == false`), + /// authority information will be contained within the path. + /// In order to populate the `Authority`, either set `hasAuthority` to + /// true on the constructor or set an empty authority before parsing. /// \param[in] _str A string. /// \return True if the string can be parsed as a URI. public: bool Parse(const std::string &_str); diff --git a/src/SystemPaths.cc b/src/SystemPaths.cc index 6c8c8cc09..ec1bf7c0e 100644 --- a/src/SystemPaths.cc +++ b/src/SystemPaths.cc @@ -313,15 +313,30 @@ std::string SystemPaths::FindFileURI(const std::string &_uri) const std::string SystemPaths::FindFileURI(const ignition::common::URI &_uri) const { std::string prefix = _uri.Scheme(); - std::string suffix = _uri.Path().Str() + _uri.Query().Str(); - std::string filename; - - if (prefix == "file") + std::string suffix; + if (_uri.Authority()) { - // First try to find the file on the current system - filename = this->FindFile(ignition::common::copyFromUnixPath(suffix)); + // Strip // + suffix = _uri.Authority()->Str().substr(2) + _uri.Path().Str(); } - else if (this->dataPtr->findFileURICB) + else + { + // Strip / + if (_uri.Path().IsAbsolute() && prefix != "file") + suffix += _uri.Path().Str().substr(1); + else + suffix += _uri.Path().Str(); + } + suffix += _uri.Query().Str(); + + std::string filename; + + // First try to find the file on the current system + filename = this->FindFile(ignition::common::copyFromUnixPath(suffix), + true, false); + + // Try URI callback + if (filename.empty() && this->dataPtr->findFileURICB) { filename = this->dataPtr->findFileURICB(_uri.Str()); } @@ -371,7 +386,8 @@ std::string SystemPaths::FindFileURI(const ignition::common::URI &_uri) const ////////////////////////////////////////////////// std::string SystemPaths::FindFile(const std::string &_filename, - const bool _searchLocalPath) const + const bool _searchLocalPath, + const bool _verbose) const { std::string path; std::string filename = _filename; @@ -441,14 +457,20 @@ std::string SystemPaths::FindFile(const std::string &_filename, if (path.empty()) { - ignerr << "Could not resolve file [" << _filename << "]" << std::endl; + if (_verbose) + { + ignerr << "Could not resolve file [" << _filename << "]" << std::endl; + } return std::string(); } if (!exists(path)) { - ignerr << "File [" << _filename << "] resolved to path [" << path << - "] but the path does not exist" << std::endl; + if (_verbose) + { + ignerr << "File [" << _filename << "] resolved to path [" << path << + "] but the path does not exist" << std::endl; + } return std::string(); } diff --git a/src/SystemPaths_TEST.cc b/src/SystemPaths_TEST.cc index d22dd4c7f..f20878fb0 100644 --- a/src/SystemPaths_TEST.cc +++ b/src/SystemPaths_TEST.cc @@ -126,8 +126,8 @@ TEST_F(SystemPathsFixture, FileSystemPaths) filePaths.push_back("test_dir1"); common::setenv(kFilePath, SystemPathsJoin(filePaths)); paths.SetFilePathEnv(kFilePath); - EXPECT_EQ(file1, paths.FindFile("test_f1")) << paths.FindFile("test_f1"); - EXPECT_EQ(file1, paths.FindFile("model:test_f1")); + EXPECT_EQ(file1, paths.FindFile("test_f1")); + EXPECT_EQ(file1, paths.FindFile("model://test_f1")); } ///////////////////////////////////////////////// @@ -234,7 +234,8 @@ TEST_F(SystemPathsFixture, FindFileURI) auto osrfCb = [dir2](const ignition::common::URI &_uri) { return _uri.Scheme() == "osrf" ? - ignition::common::joinPaths(dir2, _uri.Path().Str()) : ""; + ignition::common::joinPaths(dir2, ignition::common::copyFromUnixPath( + _uri.Path().Str())) : ""; }; auto robot2Cb = [dir2](const ignition::common::URI &_uri) { @@ -243,7 +244,7 @@ TEST_F(SystemPathsFixture, FindFileURI) }; EXPECT_EQ("", sp.FindFileURI("robot://test_f1")); - EXPECT_EQ("", sp.FindFileURI("osrf:test_f2")); + EXPECT_EQ("", sp.FindFileURI("osrf://test_f2")); // We still want to test the deprecated function until it is removed. #ifndef _WIN32 @@ -256,31 +257,31 @@ TEST_F(SystemPathsFixture, FindFileURI) #endif EXPECT_EQ(file1, sp.FindFileURI("robot://test_f1")); - EXPECT_EQ("", sp.FindFileURI("osrf:test_f2")); + EXPECT_EQ("", sp.FindFileURI("osrf://test_f2")); sp.AddFindFileURICallback(osrfCb); EXPECT_EQ(file1, sp.FindFileURI("robot://test_f1")); - EXPECT_EQ(file2, sp.FindFileURI("osrf:test_f2")); + EXPECT_EQ(file2, sp.FindFileURI("osrf://test_f2")); // Test that th CB from SetFindFileURICallback is called first even when a // second handler for the same protocol is available sp.AddFindFileURICallback(robot2Cb); EXPECT_EQ(file1, sp.FindFileURI("robot://test_f1")); - EXPECT_EQ(file2, sp.FindFileURI("osrf:test_f2")); + EXPECT_EQ(file2, sp.FindFileURI("osrf://test_f2")); // URI + env var common::setenv(kFilePath, dir1); sp.SetFilePathEnv(kFilePath); EXPECT_EQ(kFilePath, sp.FilePathEnv()); - EXPECT_EQ(file1, sp.FindFileURI("anything:test_f1")); - EXPECT_NE(file2, sp.FindFileURI("anything:test_f2")); + EXPECT_EQ(file1, sp.FindFileURI("anything://test_f1")); + EXPECT_NE(file2, sp.FindFileURI("anything://test_f2")); std::string newEnv{"IGN_NEW_FILE_PATH"}; common::setenv(newEnv, dir2); sp.SetFilePathEnv(newEnv); EXPECT_EQ(newEnv, sp.FilePathEnv()); - EXPECT_NE(file1, sp.FindFileURI("anything:test_f1")); - EXPECT_EQ(file2, sp.FindFileURI("anything:test_f2")); + EXPECT_NE(file1, sp.FindFileURI("anything://test_f1")); + EXPECT_EQ(file2, sp.FindFileURI("anything://test_f2")); } ////////////////////////////////////////////////// @@ -317,6 +318,7 @@ TEST_F(SystemPathsFixture, FindFile) EXPECT_EQ("", sp.FindFile(this->filesystemRoot + "no_such_file")); EXPECT_EQ("", sp.FindFile("no_such_file")); EXPECT_EQ(file1, sp.FindFile(common::joinPaths("test_dir1", "test_f1"))); + EXPECT_EQ(file1, sp.FindFile("file:test_dir1/test_f1")); // Existing absolute paths diff --git a/src/URI.cc b/src/URI.cc index 30606d733..3eac8d57b 100644 --- a/src/URI.cc +++ b/src/URI.cc @@ -92,7 +92,7 @@ class ignition::common::URIPrivate public: std::string scheme; /// \brief Authority component. - public: URIAuthority authority; + public: std::optional authority; /// \brief Path component. public: URIPath path; @@ -194,7 +194,7 @@ std::string URIAuthority::Str() const if (!this->dataPtr->host.empty() || (this->dataPtr->emptyHostValid && this->dataPtr->hasEmptyHost)) { - std::string result = "//"; + std::string result = kAuthDelim; result += this->dataPtr->userInfo.empty() ? "" : this->dataPtr->userInfo + "@"; result += this->dataPtr->host; @@ -232,7 +232,7 @@ bool URIAuthority::Valid(const std::string &_str, bool _emptyHostValid) return true; // The authority must start with two forward slashes - if (str.find("//") != 0) + if (str.find(kAuthDelim) != 0) return false; auto userInfoIndex = str.find("@", 2); @@ -333,7 +333,7 @@ bool URIAuthority::Parse(const std::string &_str, bool _emptyHostValid) this->dataPtr->emptyHostValid = _emptyHostValid; - if (_str.empty() || _str == "//") + if (_str.empty() || _str == kAuthDelim) { this->dataPtr->hasEmptyHost = true; return true; @@ -513,6 +513,28 @@ void URIPath::PushBack(const std::string &_part) this->dataPtr->path.push_back(part); } +///////////////////////////////////////////////// +std::string URIPath::PopFront() +{ + if (this->dataPtr->path.size() == 0) + return std::string(); + + auto result = this->dataPtr->path.front(); + this->dataPtr->path.pop_front(); + return result; +} + +///////////////////////////////////////////////// +std::string URIPath::PopBack() +{ + if (this->dataPtr->path.size() == 0) + return std::string(); + + auto result = this->dataPtr->path.back(); + this->dataPtr->path.pop_back(); + return result; +} + ///////////////////////////////////////////////// const URIPath URIPath::operator/(const std::string &_part) const { @@ -572,6 +594,7 @@ void URIPath::Clear() { this->dataPtr->path.clear(); this->dataPtr->isAbsolute = false; + this->dataPtr->trailingSlash = false; } ///////////////////////////////////////////////// @@ -906,9 +929,11 @@ URI::URI() } ///////////////////////////////////////////////// -URI::URI(const std::string &_str) +URI::URI(const std::string &_str, bool _hasAuthority) : URI() { + if (_hasAuthority) + this->dataPtr->authority.emplace(URIAuthority()); this->Parse(_str); } @@ -928,10 +953,22 @@ URI::~URI() std::string URI::Str() const { std::string result = - this->dataPtr->scheme.empty() ? "" : this->dataPtr->scheme + ":"; - result += this->dataPtr->authority.Str() + - this->dataPtr->path.Str() + - this->dataPtr->query.Str() + + this->dataPtr->scheme.empty() ? "" : this->dataPtr->scheme + kSchemeDelim; + + if (this->dataPtr->authority) + { + result += this->dataPtr->authority->Str() + this->dataPtr->path.Str(); + } + else + { + if (!this->dataPtr->scheme.empty()) + { + result += kAuthDelim; + } + result += this->dataPtr->path.Str(); + } + + result += this->dataPtr->query.Str() + this->dataPtr->fragment.Str(); return result; } @@ -949,13 +986,13 @@ void URI::SetScheme(const std::string &_scheme) } ///////////////////////////////////////////////// -URIAuthority &URI::Authority() +void URI::SetAuthority(const URIAuthority &_authority) { - return this->dataPtr->authority; + this->dataPtr->authority.emplace(_authority); } ///////////////////////////////////////////////// -const URIAuthority &URI::Authority() const +std::optional URI::Authority() const { return this->dataPtr->authority; } @@ -1000,7 +1037,9 @@ const URIFragment &URI::Fragment() const void URI::Clear() { this->dataPtr->scheme.clear(); - this->dataPtr->authority.Clear(); + // Set to empty instead of removing value + if (this->dataPtr->authority) + this->dataPtr->authority = URIAuthority(); this->dataPtr->path.Clear(); this->dataPtr->query.Clear(); this->dataPtr->fragment.Clear(); @@ -1140,31 +1179,41 @@ bool URI::Parse(const std::string &_str) emptyHostValid = true; bool authorityPresent = false; - - // Get the authority delimiter position, if one is present - size_t authDelimPos = str.find(kAuthDelim); - if (authDelimPos != std::string::npos && authDelimPos == 0) + if (this->dataPtr->authority) { - authorityPresent = true; - size_t authEndPos = str.find_first_of("/?#", - authDelimPos + std::strlen(kAuthDelim)); - - // This could be a windows file path of the form file://D:/path/to/file - // In this case, the authority is not present. - if (str[authEndPos-1] == ':' && str[authEndPos] == '/') - { - str.erase(0, std::strlen(kAuthDelim)); - authorityPresent = false; - } - else if (localScheme != "file" && authEndPos == authDelimPos+2) + // Get the authority delimiter position, if one is present + size_t authDelimPos = str.find(kAuthDelim); + if (authDelimPos != std::string::npos && authDelimPos == 0) { - ignerr << "A host is manadatory when using a scheme other than file\n"; - return false; + authorityPresent = true; + size_t authEndPos = str.find_first_of("/?#", + authDelimPos + std::strlen(kAuthDelim)); + + // This could be a windows file path of the form file://D:/path/to/file + // In this case, the authority is not present. + if (str[authEndPos-1] == ':' && str[authEndPos] == '/') + { + str.erase(0, std::strlen(kAuthDelim)); + authorityPresent = false; + } + else if (localScheme != "file" && authEndPos == authDelimPos+2) + { + ignerr << "A host is manadatory when using a scheme other than file\n"; + return false; + } + else + { + localAuthority = str.substr(authDelimPos, authEndPos); + str.erase(0, authEndPos); + } } - else + } + else + { + // Relative paths: remove `//` prefix if there are exactly 2 + if (str.find("//") == 0 && str.find("///") == std::string::npos) { - localAuthority = str.substr(authDelimPos, authEndPos); - str.erase(0, authEndPos); + str = str.substr(2); } } @@ -1192,9 +1241,9 @@ bool URI::Parse(const std::string &_str) this->Clear(); this->SetScheme(localScheme); - if (!emptyHostValid || authorityPresent) + if (this->dataPtr->authority && (!emptyHostValid || authorityPresent)) { - if (!this->dataPtr->authority.Parse(localAuthority, emptyHostValid)) + if (!this->dataPtr->authority->Parse(localAuthority, emptyHostValid)) return false; } diff --git a/src/URI_TEST.cc b/src/URI_TEST.cc index a2a87983c..b4c76ff1d 100644 --- a/src/URI_TEST.cc +++ b/src/URI_TEST.cc @@ -16,6 +16,7 @@ */ #include +#include "ignition/common/Console.hh" #include "ignition/common/URI.hh" using namespace ignition; @@ -73,6 +74,9 @@ TEST(URITEST, URIPath) EXPECT_EQ(path6.Str(), "/"); EXPECT_TRUE(path6.IsAbsolute()); + EXPECT_TRUE(path6.PopFront().empty()); + EXPECT_TRUE(path6.PopBack().empty()); + URIPath path7; path7.PushFront("/abs"); EXPECT_EQ(path7.Str(), "/abs"); @@ -116,6 +120,11 @@ TEST(URITEST, URIPath) EXPECT_EQ(path7.Str(), "/abs6%2Fabs5/abs4/abs3/abs2/abs"); EXPECT_TRUE(path7.IsAbsolute()); + EXPECT_EQ("abs6%2Fabs5", path7.PopFront()); + EXPECT_EQ("/abs4/abs3/abs2/abs", path7.Str()); + EXPECT_EQ("abs", path7.PopBack()); + EXPECT_EQ("/abs4/abs3/abs2", path7.Str()); + URIPath path8; path8.PushBack("/abs"); EXPECT_EQ(path8.Str(), "/abs"); @@ -444,14 +453,18 @@ TEST(URITEST, Scheme) EXPECT_TRUE(uri.Str().empty()); uri.SetScheme("data"); - EXPECT_EQ(uri.Str(), "data:"); + EXPECT_EQ(uri.Str(), "data://"); EXPECT_EQ("data", uri.Scheme()); } ///////////////////////////////////////////////// -TEST(URITEST, Path) +TEST(URITEST, PathIfHasAuthority) { URI uri; + EXPECT_FALSE(uri.Authority()); + uri.SetAuthority(URIAuthority()); + EXPECT_TRUE(uri.Authority()); + uri.SetScheme("data"); uri.Path() = uri.Path() / "world"; @@ -461,18 +474,25 @@ TEST(URITEST, Path) EXPECT_EQ(uri.Str(), "data:world/default"); EXPECT_TRUE(uri.Parse("file:///var/run/test")); + EXPECT_TRUE(uri.Authority()); + EXPECT_EQ(uri.Str(), "file:///var/run/test"); + EXPECT_TRUE(uri.Path().IsAbsolute()); EXPECT_TRUE(uri.Parse("file:/var/run/test")); + EXPECT_TRUE(uri.Authority()); EXPECT_EQ(uri.Str(), "file:/var/run/test"); EXPECT_TRUE(uri.Path().IsAbsolute()); EXPECT_TRUE(uri.Parse("file://var/run/test")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ(uri.Str(), "file://var/run/test"); + EXPECT_EQ(uri.Authority()->Str(), "//var"); EXPECT_EQ(uri.Path().Str(), "/run/test"); EXPECT_TRUE(uri.Path().IsAbsolute()); + EXPECT_TRUE(uri.Parse("file://test%20space")); - EXPECT_EQ(uri.Authority().Str(), "//test%20space"); - EXPECT_TRUE(uri.Path().Str().empty()); + EXPECT_EQ("//test%20space", uri.Authority()->Str()); + EXPECT_EQ("", uri.Path().Str()); EXPECT_TRUE(uri.Parse("file:///abs/path/test")); EXPECT_EQ(uri.Str(), "file:///abs/path/test"); @@ -501,6 +521,61 @@ TEST(URITEST, Path) EXPECT_TRUE(uri.Path().IsAbsolute()); } +///////////////////////////////////////////////// +TEST(URITEST, Path) +{ + URI uri; + uri.SetScheme("data"); + + uri.Path() = uri.Path() / "world"; + EXPECT_EQ(uri.Str(), "data://world"); + + uri.Path() /= "default"; + EXPECT_EQ(uri.Str(), "data://world/default"); + + EXPECT_TRUE(uri.Parse("file:///var/run/test")); + EXPECT_EQ(uri.Str(), "file:///var/run/test"); + + EXPECT_TRUE(uri.Parse("file:/var/run/test")); + EXPECT_EQ(uri.Str(), "file:///var/run/test"); + EXPECT_TRUE(uri.Path().IsAbsolute()); + + EXPECT_TRUE(uri.Parse("file://var/run/test")); + EXPECT_EQ(uri.Str(), "file://var/run/test"); + EXPECT_FALSE(uri.Authority()); + EXPECT_EQ(uri.Path().Str(), "var/run/test"); + EXPECT_FALSE(uri.Path().IsAbsolute()); + + EXPECT_TRUE(uri.Parse("file://test%20space")); + EXPECT_EQ("test%20space", uri.Path().Str()); + + EXPECT_TRUE(uri.Parse("file:///abs/path/test")); + EXPECT_EQ(uri.Str(), "file:///abs/path/test"); + EXPECT_EQ(uri.Path().Str(), "/abs/path/test"); + EXPECT_TRUE(uri.Path().IsAbsolute()); + + uri.Parse("file:/var/run/test"); + EXPECT_EQ(uri.Str(), "file:///var/run/test"); + EXPECT_EQ(uri.Path().Str(), "/var/run/test"); + EXPECT_TRUE(uri.Path().IsAbsolute()); + + EXPECT_FALSE(uri.Parse("file://test+space")); + + EXPECT_TRUE(uri.Parse("file:/test+space")); + EXPECT_EQ(uri.Str(), "file:///test+space"); + EXPECT_EQ(uri.Path().Str(), "/test+space"); + EXPECT_TRUE(uri.Path().IsAbsolute()); + + EXPECT_TRUE(uri.Parse("file:/test%20space")); + EXPECT_EQ(uri.Str(), "file:///test%20space"); + EXPECT_EQ(uri.Path().Str(), "/test%20space"); + EXPECT_TRUE(uri.Path().IsAbsolute()); + + uri.Parse("file://C:/Users"); + EXPECT_EQ(uri.Str(), "file://C:/Users"); + EXPECT_TRUE(uri.Path().IsAbsolute()); +} + ///////////////////////////////////////////////// TEST(URITEST, PathCopy) { @@ -523,21 +598,21 @@ TEST(URITEST, Query) uri.SetScheme("data"); uri.Query().Insert("p", "v"); - EXPECT_EQ(uri.Str(), "data:?p=v"); + EXPECT_EQ(uri.Str(), "data://?p=v"); uri.Path().PushFront("default"); - EXPECT_EQ(uri.Str(), "data:default?p=v"); + EXPECT_EQ(uri.Str(), "data://default?p=v"); uri.Path().PushFront("world"); - EXPECT_EQ(uri.Str(), "data:world/default?p=v"); + EXPECT_EQ(uri.Str(), "data://world/default?p=v"); URI uri2 = uri; uri.Path().Clear(); - EXPECT_EQ(uri.Str(), "data:?p=v"); + EXPECT_EQ(uri.Str(), "data://?p=v"); uri.Query().Clear(); - EXPECT_EQ(uri.Str(), "data:"); + EXPECT_EQ(uri.Str(), "data://"); uri.Clear(); uri2.Clear(); @@ -568,21 +643,21 @@ TEST(URITEST, Fragment) uri.SetScheme("data"); uri.Fragment() = "#f"; - EXPECT_EQ(uri.Str(), "data:#f"); + EXPECT_EQ(uri.Str(), "data://#f"); uri.Path().PushFront("default"); - EXPECT_EQ(uri.Str(), "data:default#f"); + EXPECT_EQ(uri.Str(), "data://default#f"); uri.Path().PushFront("world"); - EXPECT_EQ(uri.Str(), "data:world/default#f"); + EXPECT_EQ(uri.Str(), "data://world/default#f"); URI uri2 = uri; uri.Path().Clear(); - EXPECT_EQ(uri.Str(), "data:#f"); + EXPECT_EQ(uri.Str(), "data://#f"); uri.Fragment().Clear(); - EXPECT_EQ(uri.Str(), "data:"); + EXPECT_EQ(uri.Str(), "data://"); uri.Clear(); uri2.Clear(); @@ -603,8 +678,8 @@ TEST(URITEST, FragmentCopy) EXPECT_NE(uri.Fragment().Str(), fragmentCopy.Str()); EXPECT_EQ(uri.Fragment().Str(), "#f"); - EXPECT_EQ(uriTmp.Str(), "data:#g"); - EXPECT_EQ(uriCopy.Str(), "data:#g"); + EXPECT_EQ(uriTmp.Str(), "data://#g"); + EXPECT_EQ(uriCopy.Str(), "data://#g"); EXPECT_EQ(fragmentCopy.Str(), "#g"); } @@ -666,59 +741,67 @@ TEST(URITEST, URIString) TEST(URITEST, WikipediaTests) { URI uri; + uri.SetAuthority(URIAuthority()); // The following tests were pulled from: // https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Examples EXPECT_TRUE(uri.Parse("https://john.doe@www.example.com:123/forum/questions" "/?tag=networking&order=newest#top")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("https", uri.Scheme()); - EXPECT_EQ("john.doe", uri.Authority().UserInfo()); - EXPECT_EQ("www.example.com", uri.Authority().Host()); - EXPECT_EQ(123, *uri.Authority().Port()); - EXPECT_EQ("//john.doe@www.example.com:123", uri.Authority().Str()); + EXPECT_EQ("john.doe", uri.Authority()->UserInfo()); + EXPECT_EQ("www.example.com", uri.Authority()->Host()); + EXPECT_EQ(123, *uri.Authority()->Port()); + EXPECT_EQ("//john.doe@www.example.com:123", uri.Authority()->Str()); EXPECT_EQ("/forum/questions/", uri.Path().Str()); EXPECT_EQ("?tag=networking&order=newest", uri.Query().Str()); EXPECT_EQ("#top", uri.Fragment().Str()); EXPECT_TRUE(uri.Parse("ldap://[2001:db8::7]/c=GB?objectClass?one")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("ldap", uri.Scheme()); - EXPECT_EQ("[2001:db8::7]", uri.Authority().Host()); - EXPECT_EQ("//[2001:db8::7]", uri.Authority().Str()); + EXPECT_EQ("[2001:db8::7]", uri.Authority()->Host()); + EXPECT_EQ("//[2001:db8::7]", uri.Authority()->Str()); EXPECT_EQ("/c=GB", uri.Path().Str()); EXPECT_EQ("?objectClass?one", uri.Query().Str()); EXPECT_TRUE(uri.Parse("mailto:John.Doe@example.com")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("mailto", uri.Scheme()); - EXPECT_TRUE(uri.Authority().Str().empty()); + EXPECT_TRUE(uri.Authority()->Str().empty()); EXPECT_EQ("John.Doe@example.com", uri.Path().Str()); EXPECT_TRUE(uri.Query().Str().empty()); EXPECT_TRUE(uri.Fragment().Str().empty()); EXPECT_TRUE(uri.Parse("news:comp.infosystems.www.servers.unix")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("news", uri.Scheme()); - EXPECT_TRUE(uri.Authority().Str().empty()); + EXPECT_TRUE(uri.Authority()->Str().empty()); EXPECT_EQ("comp.infosystems.www.servers.unix", uri.Path().Str()); EXPECT_TRUE(uri.Query().Str().empty()); EXPECT_TRUE(uri.Fragment().Str().empty()); EXPECT_TRUE(uri.Parse("tel:+1-816-555-1212")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("tel", uri.Scheme()); - EXPECT_TRUE(uri.Authority().Str().empty()); + EXPECT_TRUE(uri.Authority()->Str().empty()); EXPECT_EQ("+1-816-555-1212", uri.Path().Str()); EXPECT_TRUE(uri.Query().Str().empty()); EXPECT_TRUE(uri.Fragment().Str().empty()); EXPECT_TRUE(uri.Parse("telnet://192.0.2.16:80/")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("telnet", uri.Scheme()); - EXPECT_TRUE(uri.Authority().UserInfo().empty()); - EXPECT_EQ("192.0.2.16", uri.Authority().Host()); - EXPECT_EQ(80, uri.Authority().Port()); + EXPECT_TRUE(uri.Authority()->UserInfo().empty()); + EXPECT_EQ("192.0.2.16", uri.Authority()->Host()); + EXPECT_EQ(80, uri.Authority()->Port()); EXPECT_EQ("/", uri.Path().Str()); EXPECT_TRUE(uri.Parse("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("urn", uri.Scheme()); - EXPECT_TRUE(uri.Authority().Str().empty()); + EXPECT_TRUE(uri.Authority()->Str().empty()); EXPECT_EQ("oasis:names:specification:docbook:dtd:xml:4.1.2", uri.Path().Str()); EXPECT_TRUE(uri.Query().Str().empty()); @@ -758,32 +841,172 @@ TEST(URITEST, URIAuthority) EXPECT_TRUE(auth2.Valid()); } +////////////////////////////////////////////////// TEST(URITEST, File) { URI uri; + uri.SetAuthority(URIAuthority()); + ASSERT_TRUE(uri.Authority()); + EXPECT_TRUE(uri.Parse("file:relative/path")); EXPECT_EQ("relative/path", uri.Path().Str()); - EXPECT_FALSE(uri.Authority().EmptyHostValid()); + EXPECT_FALSE(uri.Authority()->EmptyHostValid()); EXPECT_TRUE(uri.Parse("file:/abs/path")); EXPECT_EQ("/abs/path", uri.Path().Str()); - EXPECT_FALSE(uri.Authority().EmptyHostValid()); + EXPECT_FALSE(uri.Authority()->EmptyHostValid()); // Empty host is valid for file: scheme EXPECT_TRUE(uri.Parse("file:///abs/path")); EXPECT_EQ("/abs/path", uri.Path().Str()); - EXPECT_TRUE(uri.Authority().EmptyHostValid()); + EXPECT_TRUE(uri.Authority()->EmptyHostValid()); } ////////////////////////////////////////////////// TEST(URITEST, WinPath) { - const auto uri = ignition::common::URI("file://D:/my/test/dir/world.sdf"); + // Windows path requires authority + const auto uri = ignition::common::URI("file://D:/my/test/dir/world.sdf", + true); + ASSERT_TRUE(uri.Authority()); EXPECT_EQ("file", uri.Scheme()); - EXPECT_TRUE(uri.Authority().Str().empty()); + EXPECT_TRUE(uri.Authority()->Str().empty()); EXPECT_EQ("file:D:/my/test/dir/world.sdf", uri.Str()); } +////////////////////////////////////////////////// +TEST(URITEST, HasAuthority) +{ + { + // No authority by default + URI uri("https://john.doe@www.example.com:123/forum/questions/"); + + EXPECT_FALSE(uri.Authority()); + EXPECT_EQ("john.doe@www.example.com:123/forum/questions/", + uri.Path().Str()); + EXPECT_EQ("https://john.doe@www.example.com:123/forum/questions/", + uri.Str()); + + // Modifyng path updates string + uri.Path() = URIPath("new_authority.com/another/path"); + + EXPECT_EQ("new_authority.com/another/path/", uri.Path().Str()); + EXPECT_EQ("https://new_authority.com/another/path/", uri.Str()); + + // Clearing keeps false authority + uri.Clear(); + EXPECT_EQ("", uri.Str()); + EXPECT_FALSE(uri.Authority()); + } + + { + // Has authority + URI uri("https://john.doe@www.example.com:123/forum/questions/", true); + + ASSERT_TRUE(uri.Authority()); + EXPECT_EQ("john.doe", uri.Authority()->UserInfo()); + EXPECT_EQ("www.example.com", uri.Authority()->Host()); + EXPECT_EQ(123, *uri.Authority()->Port()); + EXPECT_EQ("//john.doe@www.example.com:123", uri.Authority()->Str()); + + EXPECT_EQ("/forum/questions/", uri.Path().Str()); + + uri.SetAuthority(URIAuthority("//new_authority.com")); + EXPECT_EQ("//new_authority.com", uri.Authority()->Str()); + EXPECT_EQ("/forum/questions/", uri.Path().Str()); + EXPECT_EQ("https://new_authority.com/forum/questions/", uri.Str()); + + // Clearing keeps true authority, but empty + uri.Clear(); + EXPECT_EQ("", uri.Str()); + ASSERT_TRUE(uri.Authority()); + EXPECT_EQ("", uri.Authority()->Str()); + } +} + +////////////////////////////////////////////////// +TEST(URITEST, Resource) +{ + // Test URIs that are commonly used for resources in Ignition + { + URI uri; + EXPECT_TRUE(uri.Parse("model://model_name/meshes/mesh.dae")); + EXPECT_EQ("model", uri.Scheme()); + EXPECT_FALSE(uri.Authority()); + EXPECT_EQ("model_name/meshes/mesh.dae", uri.Path().Str()); + EXPECT_TRUE(uri.Query().Str().empty()); + EXPECT_TRUE(uri.Fragment().Str().empty()); + EXPECT_FALSE(uri.Path().IsAbsolute()); + } + + { + URI uri; + uri.SetAuthority(URIAuthority()); + EXPECT_TRUE(uri.Parse("model://model_name/meshes/mesh.dae")); + EXPECT_EQ("model", uri.Scheme()); + ASSERT_TRUE(uri.Authority()); + EXPECT_EQ("//model_name", uri.Authority()->Str()); + EXPECT_EQ("model_name", uri.Authority()->Host()); + EXPECT_TRUE(uri.Authority()->UserInfo().empty()); + EXPECT_EQ("/meshes/mesh.dae", uri.Path().Str()); + EXPECT_TRUE(uri.Query().Str().empty()); + EXPECT_TRUE(uri.Fragment().Str().empty()); + EXPECT_TRUE(uri.Path().IsAbsolute()); + } + + { + URI uri; + EXPECT_TRUE(uri.Parse("model://model_name")); + EXPECT_EQ("model", uri.Scheme()); + EXPECT_FALSE(uri.Authority()); + EXPECT_EQ("model_name", uri.Path().Str()); + EXPECT_TRUE(uri.Query().Str().empty()); + EXPECT_TRUE(uri.Fragment().Str().empty()); + EXPECT_FALSE(uri.Path().IsAbsolute()); + } + + { + URI uri; + uri.SetAuthority(URIAuthority()); + EXPECT_TRUE(uri.Parse("model://model_name")); + EXPECT_EQ("model", uri.Scheme()); + ASSERT_TRUE(uri.Authority()); + EXPECT_EQ("//model_name", uri.Authority()->Str()); + EXPECT_EQ("model_name", uri.Authority()->Host()); + EXPECT_TRUE(uri.Authority()->UserInfo().empty()); + EXPECT_TRUE(uri.Path().Str().empty()); + EXPECT_TRUE(uri.Query().Str().empty()); + EXPECT_TRUE(uri.Fragment().Str().empty()); + EXPECT_FALSE(uri.Path().IsAbsolute()); + } + + { + URI uri; + EXPECT_TRUE(uri.Parse("package://package_name/models/model")); + EXPECT_EQ("package", uri.Scheme()); + EXPECT_FALSE(uri.Authority()); + EXPECT_EQ("package_name/models/model", uri.Path().Str()); + EXPECT_TRUE(uri.Query().Str().empty()); + EXPECT_TRUE(uri.Fragment().Str().empty()); + EXPECT_FALSE(uri.Path().IsAbsolute()); + } + + { + URI uri; + uri.SetAuthority(URIAuthority()); + EXPECT_TRUE(uri.Parse("package://package_name/models/model")); + EXPECT_EQ("package", uri.Scheme()); + ASSERT_TRUE(uri.Authority()); + EXPECT_EQ("//package_name", uri.Authority()->Str()); + EXPECT_EQ("package_name", uri.Authority()->Host()); + EXPECT_TRUE(uri.Authority()->UserInfo().empty()); + EXPECT_EQ("/models/model", uri.Path().Str()); + EXPECT_TRUE(uri.Query().Str().empty()); + EXPECT_TRUE(uri.Fragment().Str().empty()); + EXPECT_TRUE(uri.Path().IsAbsolute()); + } +} + ///////////////////////////////////////////////// int main(int argc, char **argv) {