From b775992d63aa82f3ce7d363a47c1cb898f7cdf5e Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Tue, 23 Dec 2025 08:49:50 -0800 Subject: [PATCH 1/6] container: root: common: PluggablePath: expand parent path, refactor --- container/src/root/common/utility.hh | 52 +++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/container/src/root/common/utility.hh b/container/src/root/common/utility.hh index 0b60814..c43642e 100644 --- a/container/src/root/common/utility.hh +++ b/container/src/root/common/utility.hh @@ -301,38 +301,41 @@ class PluggablePath auto& parse() { // Parse out pseudo tag - const std::string type{m_pseudo.substr(0, m_pseudo.find('/'))}; - - std::string pruned{m_pseudo}; - pruned.erase(0, pruned.find('/') + 1); + auto pos = m_pseudo.find('/'); + throw_ex_if(!pos, "no pseudo tag"); + const std::string tag{m_pseudo.substr(0, pos)}; // Set family group - m_family = pruned; - - // Set parent directory - m_parent = pruned.substr(0, pruned.find('/')); - - // Set child (filename) - m_child = pruned.substr(pruned.find_last_of('/') + 1); + m_family = m_pseudo; + m_family.erase(0, pos + 1); + throw_ex_if(m_family.empty(), "no family found"); // Set absolute path - std::string absolute{pruned}; - if (type == "repo") + m_absolute = m_family; + if (tag == "repo") { m_is_repo = true; - absolute.insert(0, m_path.repo()); + m_absolute.insert(0, m_path.repo()); } - else if (type == "custom") + else if (tag == "custom") { m_is_custom = true; - absolute.insert(0, m_path.custom()); + m_absolute.insert(0, m_path.custom()); } else { throw_ex( - "must be of type 'repo/' or 'custom/'"); + "must be of tag 'repo' or 'custom' | was given: '" + tag + "'"); } - m_absolute = std::move(absolute); + + // Set parent path (director(y|ies)) + pos = m_family.find_last_of('/'); + m_parent = m_family.substr(0, pos); + + // Set child (filename) + m_child = m_family.substr(pos + 1); + throw_ex_if( + m_child.empty() || m_child == m_parent, "child not found"); return *this; } @@ -365,22 +368,21 @@ class PluggablePath const auto& operator()() const { return m_path; } public: - //! \return The pluggable's pseudo-path + //! \return The pluggable's complete pseudo-path const std::string& pseudo() const { return m_pseudo; } - //! \return The pluggable's operating system absolute path + //! \return The pluggable's operating system absolute path (with parsed pseudo-path) const std::string& absolute() const { return m_absolute; } - // TODO(unassigned): relative() to current working dir - - //! \return The pluggable's parent directory + //! \return The pluggable's relative parent director(y|ies) derived from pseudo-path + //! \warning Trailing slash is removed //! \note This also represents the expected namespace used for pluggable auto-loading const std::string& parent() const { return m_parent; } //! \return The pluggable's child (filename) const std::string& child() const { return m_child; } - //! \return The pluggable's group of parent member and child filename + //! \return The pluggable's group of parent and child members const std::string& family() const { return m_family; } //! \return true if pseudo-path describes a repository location @@ -392,7 +394,7 @@ class PluggablePath private: type::PluggablePath m_path; std::string m_pseudo, m_absolute; - std::string m_parent, m_family, m_child; + std::string m_family, m_parent, m_child; bool m_is_repo, m_is_custom; }; From d2ff942fa04e7fb9d7fe53d4042dd91a2b6a05e3 Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Tue, 23 Dec 2025 09:00:41 -0800 Subject: [PATCH 2/6] container: root: test: unit: common: PluggablePath: update/add case for invalid family --- container/src/root/test/unit/utility.hh | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/container/src/root/test/unit/utility.hh b/container/src/root/test/unit/utility.hh index 5a310b8..96791a9 100644 --- a/container/src/root/test/unit/utility.hh +++ b/container/src/root/test/unit/utility.hh @@ -534,8 +534,8 @@ TEST_F(PluggablePath, Family_nested) Path repo("repo" + std::string{"/"} + nested), custom("custom" + std::string{"/"} + nested); - ASSERT_EQ(repo.parent(), kParent); - ASSERT_EQ(custom.parent(), kParent); + ASSERT_EQ(repo.parent(), kParent + "/a_nested_dir"); + ASSERT_EQ(custom.parent(), kParent + "/a_nested_dir"); ASSERT_EQ(repo.child(), kChild); ASSERT_EQ(custom.child(), kChild); @@ -544,6 +544,26 @@ TEST_F(PluggablePath, Family_nested) ASSERT_EQ(custom.family(), nested); } +TEST_F(PluggablePath, InvalidFamily) +{ + ASSERT_THROW(Path path(""), common::type::RuntimeError); + ASSERT_THROW(Path path("nope"), common::type::RuntimeError); + + ASSERT_THROW(Path path("repo/"), common::type::RuntimeError); + ASSERT_THROW(Path path("custom/"), common::type::RuntimeError); + + ASSERT_THROW(Path path("repo/incomplete"), common::type::RuntimeError); + ASSERT_THROW(Path path("custom/incomplete"), common::type::RuntimeError); + + ASSERT_THROW(Path path("repo/incomplete/"), common::type::RuntimeError); + ASSERT_THROW(Path path("custom/incomplete/"), common::type::RuntimeError); + + ASSERT_THROW( + Path path("repo/incomplete/incomplete"), common::type::RuntimeError); + ASSERT_THROW( + Path path("custom/incomplete/incomplete"), common::type::RuntimeError); +} + TEST_F(PluggablePath, Base) { ASSERT_EQ(m_repo().repo(), kRepoBase); From 8bc6477c270a013b73026a1f40623741524e863f Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Tue, 23 Dec 2025 10:54:57 -0800 Subject: [PATCH 3/6] container: root: common: PluggablePath: add checks for invalid characters --- container/src/root/common/utility.hh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/container/src/root/common/utility.hh b/container/src/root/common/utility.hh index c43642e..9bb107e 100644 --- a/container/src/root/common/utility.hh +++ b/container/src/root/common/utility.hh @@ -300,6 +300,20 @@ class PluggablePath //! \return NPI reference auto& parse() { + // Invalid characters + const std::regex regex{"[a-zA-Z0-9/_\\-\\.]+"}; + const std::string msg{"invalid characters in path"}; + + if (!m_path.repo().empty()) + throw_ex_if( + !std::regex_match(m_path.repo(), regex), msg); + + if (!m_path.custom().empty()) + throw_ex_if( + !std::regex_match(m_path.custom(), regex), msg); + + throw_ex_if(!std::regex_match(m_pseudo, regex), msg); + // Parse out pseudo tag auto pos = m_pseudo.find('/'); throw_ex_if(!pos, "no pseudo tag"); From 736a71e9d1845bc896cf800ec84719a7018cedca Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Tue, 23 Dec 2025 11:02:26 -0800 Subject: [PATCH 4/6] container: root: test: unit: common: PluggablePath: add case for invalid characters --- container/src/root/test/unit/utility.hh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/container/src/root/test/unit/utility.hh b/container/src/root/test/unit/utility.hh index 96791a9..ce9f42e 100644 --- a/container/src/root/test/unit/utility.hh +++ b/container/src/root/test/unit/utility.hh @@ -564,6 +564,25 @@ TEST_F(PluggablePath, InvalidFamily) Path path("custom/incomplete/incomplete"), common::type::RuntimeError); } +TEST_F(PluggablePath, InvalidCharacters) +{ + ASSERT_THROW(Path path("repo/no spaces/a.file"), common::type::RuntimeError); + ASSERT_THROW( + Path path("custom/no spaces/a.file"), common::type::RuntimeError); + + ASSERT_THROW(Path path("repo/'no_ticks'/a.file"), common::type::RuntimeError); + ASSERT_THROW( + Path path("custom/'no_ticks'/a.file"), common::type::RuntimeError); + + ASSERT_THROW(Path path("repo/any/@a.file"), common::type::RuntimeError); + ASSERT_THROW(Path path("custom/any/@a.file"), common::type::RuntimeError); + + ASSERT_THROW( + Path path("repo/bad\\ slash/a.file"), common::type::RuntimeError); + ASSERT_THROW( + Path path("custom/bad\\ slash/a.file"), common::type::RuntimeError); +} + TEST_F(PluggablePath, Base) { ASSERT_EQ(m_repo().repo(), kRepoBase); From ce12412d100be1628009687a7a18e6f685ca4400 Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Tue, 23 Dec 2025 11:48:05 -0800 Subject: [PATCH 5/6] container: root: common: PluggableSpace: add character checks/conversions --- container/src/root/common/utility.hh | 46 +++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/container/src/root/common/utility.hh b/container/src/root/common/utility.hh index 9bb107e..01c5cf9 100644 --- a/container/src/root/common/utility.hh +++ b/container/src/root/common/utility.hh @@ -421,8 +421,52 @@ class PluggablePath //! \since docker-finance 1.1.0 class PluggableSpace { + public: + //! \brief Parses (or re-parses) constructed types + //! \warning Only call this function after constructing if underlying type::PluggableSpace has been changed (post-construction) + //! \return NPI reference + auto& parse() + { + auto const parser = [](const std::string& space) -> std::string { + std::string parsed{space}; + + // NOTE: allowed to be empty (for now) + if (!parsed.empty()) + { + throw_ex_if( + !std::regex_match( + parsed, + std::regex{ + "[a-zA-Z0-9:/_\\-]+"} /* TODO(unassigned): refine */), + "invalid characters in namespace"); + + if (parsed.find('/')) + { + parsed = std::regex_replace(parsed, std::regex{"/"}, "::"); + } + if (parsed.find('-')) + { + parsed = std::regex_replace(parsed, std::regex{"-"}, "_"); + } + } + + return parsed; + }; + + m_space.outer(parser(m_space.outer())); + m_space.inner(parser(m_space.inner())); + + return *this; + } + protected: - explicit PluggableSpace(const type::PluggableSpace& space) : m_space(space) {} + // \note Since the current presumption is that a PluggableSpace is likely + // to be derived from an operating system path (via PluggablePath), + // there's leeway for path-to-namespace conversions to be done here. + explicit PluggableSpace(const type::PluggableSpace& space) : m_space(space) + { + parse(); + } ~PluggableSpace() = default; PluggableSpace(const PluggableSpace&) = default; From 6a7087f175ca7235fde533d05ddc7dc64b91504b Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Tue, 23 Dec 2025 11:53:49 -0800 Subject: [PATCH 6/6] container: root: test: unit: common: PluggableSpace: add case for conversions --- container/src/root/test/unit/utility.hh | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/container/src/root/test/unit/utility.hh b/container/src/root/test/unit/utility.hh index ce9f42e..52622e5 100644 --- a/container/src/root/test/unit/utility.hh +++ b/container/src/root/test/unit/utility.hh @@ -670,11 +670,10 @@ struct PluggableSpace : public ::testing::Test, struct Space : public ::dfi::common::PluggableSpace { - using space = ::dfi::tests::unit::PluggableSpace; explicit Space(const std::string_view outer, const std::string_view inner) : ::dfi::common::PluggableSpace( ::dfi::common::type::PluggableSpace{ - {std::string{space::kOuter}, std::string{space::kInner}}}) + {std::string{outer}, std::string{inner}}}) { } @@ -726,6 +725,29 @@ TEST_F(PluggableSpace, Mutators) ASSERT_EQ(m_space.inner(), kFour); } +TEST_F(PluggableSpace, Conversions) +{ + ASSERT_NO_THROW(Space space("", "")); + ASSERT_NO_THROW(Space space("hi/hey", "there/now")); + ASSERT_THROW(Space space("hi hey", "there now"), common::type::RuntimeError); + ASSERT_THROW(Space space("hi@hey", "there@now"), common::type::RuntimeError); + + Space space("hi/hey", "there/now"); + ASSERT_NO_THROW(space().outer("hi hey").inner("there now")); + ASSERT_THROW(space.parse(), common::type::RuntimeError); + ASSERT_NO_THROW(space().outer("hi@hey").inner("there@now")); + ASSERT_THROW(space.parse(), common::type::RuntimeError); + + space = Space{"hi/hey", "there/now"}; + ASSERT_EQ(space.outer(), "hi::hey"); + ASSERT_EQ(space.inner(), "there::now"); + + ASSERT_NO_THROW(space().outer("hi-hey").inner("there-now")); + ASSERT_NO_THROW(space.parse()); + ASSERT_EQ(space.outer(), "hi_hey"); + ASSERT_EQ(space.inner(), "there_now"); +} + TEST_F(PluggableSpace, Booleans) { ASSERT_EQ(m_space.has_outer(), true);