because the parameters specified for the library (or the files those
parameters point to) may have changed.
+As of Kea 2.4.2, hook libraries may only be loaded from the default installation
+directory determined during compilation and shown in the config report as
+"Hooks directory". This value may be overridden at startup by setting the
+environment variable ``KEA_HOOKS_PATH`` to the desired path. If a path other
+than this value is used in a ``library`` element Kea will emit an error and refuse
+to load the library. For ease of use ``library`` elements may simply omit path
+components, specifying the file name only as shown below:
+
+.. code-block:: json
+
+ {
+ "Dhcp6": {
+ "hooks-libraries": [
+ {
+ "library": "first_custom_hooks_example.so"
+ },
+ {
+ "library": "second_custom_hooks_example.so"
+ }
+ ]
+ }
+ }
+
+This snippet (on Debian 12) is equivalent to:
+
+.. code-block:: json
+
+ {
+ "Dhcp6": {
+ "hooks-libraries": [
+ {
+ "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/first_custom_hooks_example.so"
+ },
+ {
+ "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/second_custom_hooks_example.so"
+ }
+ ]
+ }
+ }
+
Libraries may have additional parameters that are not mandatory, in the
sense that there may be libraries that do not require them. However, for any
given library there is often a requirement to specify a certain
#include <process/testutils/d_test_stubs.h>
#include <process/d_cfg_mgr.h>
#include <http/basic_auth_config.h>
+#include <hooks/hooks_parser.h>
#include <agent/tests/test_callout_libraries.h>
#include <agent/tests/test_data_files_config.h>
#include <boost/pointer_cast.hpp>
/// @brief Class used for testing CfgMgr
class AgentParserTest : public isc::process::ConfigParseTest {
public:
+ virtual void SetUp() {
+ resetHooksPath();
+ }
+
+ virtual void TearDown() {
+ resetHooksPath();
+ }
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : CA_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
+ }
/// @brief Tries to load input text as a configuration
///
// This test checks if a config with only HTTP parameters is parsed properly.
TEST_F(AgentParserTest, configParseHttpOnly) {
+ setHooksTestPath();
+
configParse(AGENT_CONFIGS[1], 0);
CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
// name. In particular, it checks if such a library exists. Therefore we
// can't use AGENT_CONFIGS[4] as is, but need to run it through path replacer.
TEST_F(AgentParserTest, configParseHooks) {
+ setHooksTestPath();
+
// Create the configuration with proper lib path.
std::string cfg = pathReplacer(AGENT_CONFIGS[4], CALLOUT_LIBRARY);
// The configuration should be successful.
#include <process/testutils/d_test_stubs.h>
#include <agent/ca_cfg_mgr.h>
#include <agent/parser_context.h>
+#include <hooks/hooks_parser.h>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
using namespace isc::data;
using namespace isc::process;
using namespace isc::test;
+using namespace isc::hooks;
namespace {
public:
CtrlAgentGetCfgTest()
: rcode_(-1) {
+ resetHooksPath();
srv_.reset(new NakedAgentCfgMgr());
// Create fresh context.
resetConfiguration();
~CtrlAgentGetCfgTest() {
resetConfiguration();
+ resetHooksPath();
+ }
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : CA_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
}
/// @brief Parse and Execute configuration
/// Test a configuration
TEST_F(CtrlAgentGetCfgTest, simple) {
+ setHooksTestPath();
// get the simple configuration
std::string simple_file = string(CFG_EXAMPLES) + "/" + "simple.json";
// Basic callout library with context_create and three "standard" callouts.
static const char* CALLOUT_LIBRARY = "@abs_builddir@/.libs/libcallout.so";
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* CA_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
} // anonymous namespace
#endif // TEST_LIBRARIES_H
#include <d2srv/d2_config.h>
#include <d2srv/d2_simple_parser.h>
#include <dhcpsrv/testutils/config_result_check.h>
+#include <hooks/hooks_parser.h>
#include <process/testutils/d_test_stubs.h>
#include <test_data_files_config.h>
#include <util/encode/base64.h>
/// @brief Constructor
D2CfgMgrTest():cfg_mgr_(new D2CfgMgr()), d2_params_() {
+ resetHooksPath();
}
/// @brief Destructor
~D2CfgMgrTest() {
+ resetHooksPath();
+ }
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : D2_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
}
/// @brief Configuration manager instance.
/// as it would be done by d2_process in response to a configuration update
/// event.
TEST_F(D2CfgMgrTest, fullConfig) {
+ setHooksTestPath();
+
// Create a configuration with all of application level parameters, plus
// both the forward and reverse ddns managers. Both managers have two
// domains with three servers per domain.
CFG_FILE="@abs_top_builddir@/src/bin/d2/tests/test_config.json"
# Path to the D2 log file.
LOG_FILE="@abs_top_builddir@/src/bin/d2/tests/test.log"
+# Set env KEA_HOOKS_PATH to override DEFAULT_HOOKS_PATH
+export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/d2/tests/.libs"
# D2 configuration to be stored in the configuration file.
CONFIG="{
\"DhcpDdns\":
#include <d2/d2_process.h>
#include <d2/tests/test_configured_libraries.h>
#include <dhcp_ddns/ncr_io.h>
+#include <hooks/hooks_parser.h>
#include <process/testutils/d_test_stubs.h>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace isc::d2;
using namespace isc::data;
using namespace isc::process;
+using namespace isc::hooks;
using namespace boost::posix_time;
namespace {
D2ProcessTest() :
D2Process("d2test",
asiolink::IOServicePtr(new isc::asiolink::IOService())) {
+ resetHooksPath();
}
/// @brief Destructor
virtual ~D2ProcessTest() {
+ resetHooksPath();
+ }
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : D2_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
}
/// @brief Callback that will invoke shutdown method.
/// @brief Check the configured callout (positive case).
TEST_F(D2ProcessTest, configuredNoFail) {
+ setHooksTestPath();
+
const char* config = "{\n"
"\"hooks-libraries\": [ {\n"
" \"library\": \"%LIBRARY%\",\n"
/// @brief Check the configured callout (negative case).
TEST_F(D2ProcessTest, configuredFail) {
+ setHooksTestPath();
+
const char* config = "{\n"
"\"user-context\": { \"error\": \"Fail!\" },\n"
"\"hooks-libraries\": [ {\n"
#include <d2/parser_context.h>
#include <d2srv/d2_cfg_mgr.h>
#include <d2srv/d2_config.h>
+#include <hooks/hooks_parser.h>
#include <process/testutils/d_test_stubs.h>
#include <testutils/user_context_utils.h>
#include <gtest/gtest.h>
using namespace isc::d2;
using namespace isc::data;
using namespace isc::process;
+using namespace isc::hooks;
using namespace isc::test;
namespace {
public:
D2GetConfigTest()
: rcode_(-1) {
+ resetHooksPath();
srv_.reset(new D2CfgMgr());
// Enforce not verbose mode.
Daemon::setVerbose(false);
~D2GetConfigTest() {
resetConfiguration();
+ resetHooksPath();
+ }
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : D2_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
}
/// @brief Parse and Execute configuration
/// Test a configuration
TEST_F(D2GetConfigTest, sample1) {
+ setHooksTestPath();
// get the sample1 configuration
std::string sample1_file = string(CFG_EXAMPLES) + "/" + "sample1.json";
// Basic callout library with context_create and three "standard" callouts.
static const char* CALLOUT_LIBRARY = "@abs_builddir@/.libs/libcallout.so";
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* D2_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
} // anonymous namespace
#endif // D2_TEST_CALLOUT_LIBRARIES_H
// content of the error entry.
static const char* CONFIGURED_LIBRARY = "@abs_builddir@/.libs/libconfigured.so";
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* D2_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
} // anonymous namespace
#endif // D2_TEST_CONFIGURED_LIBRARIES_H
#include <dhcpsrv/testutils/test_config_backend_dhcp4.h>
#include <process/config_ctl_info.h>
#include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
#include <stats/stats_mgr.h>
#include <testutils/log_utils.h>
#include <testutils/gtest_utils.h>
virtual void SetUp() {
std::vector<std::string> libraries = HooksManager::getLibraryNames();
ASSERT_TRUE(libraries.empty());
+ resetHooksPath();
}
public:
srv_.reset(new ControlledDhcpv4Srv(0));
// Create fresh context.
resetConfiguration();
+ resetHooksPath();
}
public:
EXPECT_EQ(expected_code, rcode_) << "error text:" << comment_->stringValue();
}
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : DHCP4_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
+ }
+
// Checks if the result of DHCP server configuration has
// expected code (0 for success, other for failures) and
// the text part. Also stores result in rcode_ and comment_.
// Verify the configuration of hooks libraries with two being specified.
TEST_F(Dhcp4ParserTest, LibrariesSpecified) {
+ setHooksTestPath();
+
// Marker files should not be present.
EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
// Check that the "libreload" command will reload libraries
TEST_F(CtrlChannelDhcpv4SrvTest, libreload) {
createUnixChannelServer();
+ ASSERT_TRUE(DHCP4_HOOKS_TEST_PATH); // suppress unused warning.
// Ensure no marker files to start with.
ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
# Path to test hooks library
HOOK_PATH="@abs_top_builddir@/src/bin/dhcp4/tests/.libs/libco3.so"
+
+# Set env KEA_HOOKS_PATH to override DEFAULT_HOOKS_PATH
+export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/dhcp4/tests/.libs"
+
# Kea configuration to be stored in the configuration file.
CONFIG="{
\"Dhcp4\":
#include <dhcp4/tests/test_libraries.h>
#include <hooks/server_hooks.h>
#include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
#include <hooks/callout_manager.h>
#include <stats/stats_mgr.h>
#include <util/multi_threading_mgr.h>
LoadUnloadDhcpv4SrvTest() {
reset();
MultiThreadingMgr::instance().setMode(false);
+ resetHooksPath();
}
/// @brief Destructor
server_.reset();
reset();
MultiThreadingMgr::instance().setMode(false);
+ resetHooksPath();
};
/// @brief Reset hooks data
CfgMgr::instance().clear();
}
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : DHCP4_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
+ }
};
// Checks if callouts installed on buffer4_receive are indeed called and the
// Checks if callouts installed on the dhcp4_srv_configured ared indeed called
// and all the necessary parameters are passed.
TEST_F(LoadUnloadDhcpv4SrvTest, Dhcpv4SrvConfigured) {
+ setHooksTestPath();
for (string parameters : {
"",
R"(, "parameters": { "mode": "fail-without-error" } )",
// Name of a library which is not present.
const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* DHCP4_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
} // anonymous namespace
#include <dhcpsrv/testutils/config_result_check.h>
#include <dhcpsrv/testutils/test_config_backend_dhcp6.h>
#include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
#include <process/config_ctl_info.h>
#include <stats/stats_mgr.h>
#include <testutils/gtest_utils.h>
// Reset configuration for each test.
resetConfiguration();
+
+ resetHooksPath();
}
~Dhcp6ParserTest() {
// ... and delete the hooks library marker files if present
static_cast<void>(remove(LOAD_MARKER_FILE));
static_cast<void>(remove(UNLOAD_MARKER_FILE));
+
+ resetHooksPath();
};
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : DHCP6_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
+ }
+
+
// Checks if config_result (result of DHCP server configuration) has
// expected code (0 for success, other for failures).
// Also stores result in rcode_ and comment_.
// Verify the configuration of hooks libraries with two being specified.
TEST_F(Dhcp6ParserTest, LibrariesSpecified) {
+ setHooksTestPath();
+
// Marker files should not be present.
EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
// Check that the "libreload" command will reload libraries
TEST_F(CtrlChannelDhcpv6SrvTest, libreload) {
createUnixChannelServer();
+ ASSERT_TRUE(DHCP6_HOOKS_TEST_PATH); // suppress unused warning.
// Ensure no marker files to start with.
ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
# Path to test hooks library
HOOK_PATH="@abs_top_builddir@/src/bin/dhcp6/tests/.libs/libco3.so"
+
+# Set env KEA_HOOKS_PATH to override DEFAULT_HOOKS_PATH
+export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/dhcp6/tests/.libs"
+
# Kea configuration to be stored in the configuration file.
CONFIG="{
\"Dhcp6\":
#include <dhcp6/tests/test_libraries.h>
#include <hooks/server_hooks.h>
#include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
#include <hooks/callout_manager.h>
#include <stats/stats_mgr.h>
#include <util/buffer.h>
LoadUnloadDhcpv6SrvTest() : Dhcpv6SrvTest() {
reset();
MultiThreadingMgr::instance().setMode(false);
+
+ resetHooksPath();
}
/// @brief Destructor
server_.reset();
reset();
MultiThreadingMgr::instance().setMode(false);
+
+ resetHooksPath();
};
/// @brief Reset hooks data
CfgMgr::instance().clear();
}
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param explicit_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : DHCP6_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
+ }
};
// Checks if callouts installed on buffer6_receive are indeed called and the
// Checks if callouts installed on the dhcp6_srv_configured ared indeed called
// and all the necessary parameters are passed.
TEST_F(LoadUnloadDhcpv6SrvTest, Dhcpv6SrvConfigured) {
+ setHooksTestPath();
+
for (string parameters : {
"",
R"(, "parameters": { "mode": "fail-without-error" } )",
// Name of a library which is not present.
const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* DHCP6_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
} // anonymous namespace
AM_CPPFLAGS += -DDHCP_DATA_DIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
AM_CPPFLAGS += -DKEA_LFC_BUILD_DIR=\"$(abs_top_builddir)/src/bin/lfc\"
AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+AM_CPPFLAGS += -DDEFAULT_HOOKS_PATH=\"$(libdir)/kea/hooks\"
AM_CXXFLAGS = $(KEA_CXXFLAGS)
-// Copyright (C) 2012-2023 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
ParseConfigTest()
:family_(AF_INET6) {
reset_context();
+ resetHooksPath();
}
~ParseConfigTest() {
reset_context();
CfgMgr::instance().clear();
+ resetHooksPath();
+ }
+
+ /// @brief Sets the Hooks path from which hooks can be loaded.
+ /// @param custom_path path to use as the hooks path.
+ void setHooksTestPath(const std::string explicit_path = "") {
+ HooksLibrariesParser::getHooksPath(true,
+ (!explicit_path.empty() ?
+ explicit_path : DHCPSRV_HOOKS_TEST_PATH));
+ }
+
+ /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+ void resetHooksPath() {
+ HooksLibrariesParser::getHooksPath(true);
}
/// @brief Parses a configuration.
// hooks-libraries element that contains a single library.
TEST_F(ParseConfigTest, oneHooksLibrary) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// hooks-libraries element that contains two libraries
TEST_F(ParseConfigTest, twoHooksLibraries) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// Configure with two libraries, then reconfigure with the same libraries.
TEST_F(ParseConfigTest, reconfigureSameHooksLibraries) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// Configure the hooks with two libraries, then reconfigure with the same
// libraries, but in reverse order.
TEST_F(ParseConfigTest, reconfigureReverseHooksLibraries) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// Configure the hooks with two libraries, then reconfigure with
// no libraries.
TEST_F(ParseConfigTest, reconfigureZeroHooksLibraries) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// Check with a set of libraries, some of which are invalid.
TEST_F(ParseConfigTest, invalidHooksLibraries) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// Check that trying to reconfigure with an invalid set of libraries fails.
TEST_F(ParseConfigTest, reconfigureInvalidHooksLibraries) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
// Check that if hooks-libraries contains invalid syntax, it is detected.
TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) {
+ setHooksTestPath("/opt/lib");
// Element holds a mixture of (valid) maps and non-maps.
string config1 = "{ \"hooks-libraries\": [ "
"{ \"library\": \"/opt/lib/lib1\" }, "
"{ \"library\": \"\" } "
"] }";
- string error3 = "value of 'library' element must not be blank";
+ string error3 = "Configuration parsing failed: hooks library configuration error:"
+ " path: '' has no filename (<string>:1:69)";
rcode = parseConfiguration(config3);
ASSERT_NE(0, rcode);
"{ \"library\": \"/opt/lib/lib1\" }, "
"{ \"library\": \" \" } "
"] }";
- string error4 = "value of 'library' element must not be blank";
+ string error4 = "Configuration parsing failed: hooks library configuration error:"
+ " path: '' has no filename (<string>:1:69)";
rcode = parseConfiguration(config4);
ASSERT_NE(0, rcode);
- EXPECT_TRUE(error_text_.find(error3) != string::npos) <<
+ EXPECT_TRUE(error_text_.find(error4) != string::npos) <<
"Error text returned from parse failure is " << error_text_;
// Element holds valid maps, except one that does not contain a
// Check that some parameters may have configuration parameters configured.
TEST_F(ParseConfigTest, HooksLibrariesParameters) {
+ setHooksTestPath();
+
// Check that no libraries are currently loaded
vector<string> hooks_libraries = HooksManager::getLibraryNames();
EXPECT_TRUE(hooks_libraries.empty());
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// Name of a library which is not present.
static const char* NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* DHCPSRV_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
} // anonymous namespace
SUBDIRS = . tests
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -DDEFAULT_HOOKS_PATH=\"$(libdir)/kea/hooks\"
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
#include <hooks/hooks_parser.h>
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>
+#include <util/filesystem.h>
#include <util/strutil.h>
+
#include <vector>
using namespace std;
using namespace isc::data;
using namespace isc::hooks;
using namespace isc::dhcp;
+using namespace isc::util::file;
namespace isc {
namespace hooks {
+std::string
+HooksLibrariesParser::getHooksPath(bool reset /* = false */, const std::string explicit_path /* = "" */) {
+ static std::string default_hooks_path = "";
+ if (default_hooks_path.empty() || reset) {
+ if (explicit_path.empty()) {
+ default_hooks_path = std::string(std::getenv("KEA_HOOKS_PATH") ?
+ std::getenv("KEA_HOOKS_PATH")
+ : DEFAULT_HOOKS_PATH);
+ } else {
+ default_hooks_path = explicit_path;
+ }
+ }
+
+ return (default_hooks_path);
+}
+
// @todo use the flat style, split into list and item
void
// Get the name of the library and add it to the list after
// removing quotes.
- libname = (entry_item.second)->stringValue();
-
- // Remove leading/trailing quotes and any leading/trailing
- // spaces.
- boost::erase_all(libname, "\"");
- libname = isc::util::str::trim(libname);
- if (libname.empty()) {
+ try {
+ libname = validatePath((entry_item.second)->stringValue());
+ } catch (const std::exception& ex) {
isc_throw(DhcpConfigError, "hooks library configuration"
- " error: value of 'library' element must not be"
- " blank (" <<
- entry_item.second->getPosition() << ")");
+ " error: " << ex.what() << " ("
+ << entry_item.second->getPosition() << ")");
}
// Note we have found the library name.
}
}
+std::string
+HooksLibrariesParser::validatePath(const std::string libpath,
+ bool enforce_path /* = true */) {
+ return (FileManager::validatePath(HooksLibrariesParser::getHooksPath(),
+ libpath, enforce_path));
+}
+
}
}
-// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2017-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
/// @param libraries parsed libraries information will be stored here
/// @param value pointer to the content to be parsed
void parse(HooksConfig& libraries, isc::data::ConstElementPtr value);
+
+ /// @brief Validates a library path against the supported path for hooks libraries.
+ ///
+ /// @param libpath library path to validate.
+ /// @param enforce_path enables validation against the supported path.
+ /// If false verifies only that the path contains a file name.
+ ///
+ /// @return validated path
+ static std::string validatePath(const std::string libpath,
+ bool enforce_path = true);
+
+ /// @brief Fetches the supported Hooks path.
+ ///
+ /// The first call to this function with no arguments will set the default
+ /// hooks path to either the value of DEFAULT_HOOKS_PATH or the environment
+ /// variable KEA_HOOKS_PATH if it is defined. Subsequent calls with no
+ /// arguments will simply return this value.
+ ///
+ /// @param reset recalculate when true, defaults to false. This is for
+ /// testing purposes only.
+ /// @param explicit_path set default hooks path to this value. This is
+ /// for testing purposes only.
+ ///
+ /// @return String containing the default hooks path.
+ static std::string getHooksPath(bool reset = false,
+ const std::string explicit_path = "");
};
}; // namespace isc::hooks
SUBDIRS = .
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -DDEFAULT_HOOKS_PATH=\"$(libdir)/kea/hooks\"
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
#include <hooks/callout_handle.h>
#include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
#include <hooks/server_hooks.h>
+#include <testutils/gtest_utils.h>
#include <hooks/tests/common_test_class.h>
#define TEST_ASYNC_CALLOUT
EXPECT_FALSE(unparked);
}
+/// @brief Test fixture for hooks parsing.
+class HooksParserTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ HooksParserTest() {
+ // Save current value of the environment path.
+ char* env_path = std::getenv("KEA_HOOKS_PATH");
+ if (env_path) {
+ original_path_ = std::string(env_path);
+ }
+
+ // Clear the environment path.
+ unsetenv("KEA_HOOKS_PATH");
+ }
+
+ /// @brief Destructor
+ ~HooksParserTest() {
+ // Restore the original environment path.
+ if (!original_path_.empty()) {
+ setenv("KEA_HOOKS_PATH", original_path_.c_str(), 1);
+ } else {
+ unsetenv("KEA_HOOKS_PATH");
+ }
+ }
+
+ /// @brief Retains the environment variable's original value.
+ std::string original_path_;
+};
+
+TEST_F(HooksParserTest, getHooksPath) {
+ ASSERT_FALSE(std::getenv("KEA_HOOKS_PATH"));
+ auto hooks_path = HooksLibrariesParser::getHooksPath(true);
+ EXPECT_EQ(hooks_path, DEFAULT_HOOKS_PATH);
+}
+
+TEST_F(HooksParserTest, getHooksPathWithEnv) {
+ std::string evar("KEA_HOOKS_PATH=/tmp");
+ putenv(const_cast<char*>(evar.c_str()));
+ ASSERT_TRUE(std::getenv("KEA_HOOKS_PATH"));
+ auto hooks_path = HooksLibrariesParser::getHooksPath(true);
+ EXPECT_EQ(hooks_path, "/tmp");
+}
+
+TEST_F(HooksParserTest, getHooksPathExplicit) {
+ auto hooks_path = HooksLibrariesParser::getHooksPath(true, "/explicit/path");
+ EXPECT_EQ(hooks_path, "/explicit/path");
+}
+
+// Verifies HooksParser::validatePath() when enforce_path is true.
+TEST_F(HooksParserTest, validatePathEnforcePath) {
+ HooksLibrariesParser::getHooksPath(true);
+ std::string def_path(HooksLibrariesParser::getHooksPath());
+ struct Scenario {
+ int line_;
+ std::string lib_path_;
+ std::string exp_path_;
+ std::string exp_error_;
+ };
+
+ std::list<Scenario> scenarios = {
+ {
+ // Invalid parent path.
+ __LINE__,
+ "/var/lib/bs/mylib.so",
+ "",
+ string("invalid path specified: '/var/lib/bs', supported path is '" + def_path + "'")
+ },
+ {
+ // No file name.
+ __LINE__,
+ def_path + "/",
+ "",
+ string ("path: '" + def_path + "/' has no filename")
+ },
+ {
+ // File name only is valid.
+ __LINE__,
+ "mylib.so",
+ def_path + "/mylib.so",
+ ""
+ },
+ {
+ // Valid full path.
+ __LINE__,
+ def_path + "/mylib.so",
+ def_path + "/mylib.so",
+ ""
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ std::ostringstream oss;
+ oss << " Scenario at line: " << scenario.line_;
+ SCOPED_TRACE(oss.str());
+ std::string validated_path;
+ if (scenario.exp_error_.empty()) {
+ ASSERT_NO_THROW_LOG(validated_path =
+ HooksLibrariesParser::validatePath(scenario.lib_path_));
+ EXPECT_EQ(validated_path, scenario.exp_path_);
+ } else {
+ ASSERT_THROW_MSG(validated_path =
+ HooksLibrariesParser::validatePath(scenario.lib_path_),
+ BadValue, scenario.exp_error_);
+ }
+ }
+}
+
+// Verifies HooksParser::validatePath() when enforce_path is false.
+TEST_F(HooksParserTest, validatePathEnforcePathFalse) {
+ HooksLibrariesParser::getHooksPath(true);
+ std::string def_path(HooksLibrariesParser::getHooksPath());
+ struct Scenario {
+ int line_;
+ std::string lib_path_;
+ std::string exp_path_;
+ std::string exp_error_;
+ };
+
+ std::list<Scenario> scenarios = {
+ {
+ // Invalid parent path will fly.
+ __LINE__,
+ "/var/lib/bs/mylib.so",
+ "/var/lib/bs/mylib.so",
+ "",
+ },
+ {
+ // No file name.
+ __LINE__,
+ def_path + "/",
+ "",
+ string ("path: '" + def_path + "/' has no filename")
+ },
+ {
+ // File name only is valid.
+ __LINE__,
+ "mylib.so",
+ def_path + "/mylib.so",
+ ""
+ },
+ {
+ // Valid full path.
+ __LINE__,
+ def_path + "/mylib.so",
+ def_path + "/mylib.so",
+ ""
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ std::ostringstream oss;
+ oss << " Scenario at line: " << scenario.line_;
+ SCOPED_TRACE(oss.str());
+ std::string validated_path;
+ if (scenario.exp_error_.empty()) {
+ ASSERT_NO_THROW_LOG(validated_path =
+ HooksLibrariesParser::validatePath(scenario.lib_path_, false));
+ EXPECT_EQ(validated_path, scenario.exp_path_);
+ } else {
+ ASSERT_THROW_MSG(validated_path =
+ HooksLibrariesParser::validatePath(scenario.lib_path_, false),
+ BadValue, scenario.exp_error_);
+ }
+ }
+}
} // Anonymous namespace
libkea_util_la_SOURCES += encode/binary_from_base32hex.h
libkea_util_la_SOURCES += encode/binary_from_base16.h
libkea_util_la_SOURCES += encode/utf8.cc encode/utf8.h
+libkea_util_la_SOURCES += filesystem.h filesystem.cc
libkea_util_la_LIBADD = $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
--- /dev/null
+// Copyright (C) 2021-2025 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <exceptions/exceptions.h>
+#include <util/filesystem.h>
+#include <util/strutil.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <fstream>
+#include <string>
+#include <filesystem>
+#include <iostream>
+
+#include <dirent.h>
+#include <fcntl.h>
+
+using namespace isc;
+using namespace isc::util::str;
+using namespace std;
+
+namespace isc {
+namespace util {
+namespace file {
+
+bool
+exists(string const& path) {
+ struct stat statbuf;
+ return (::stat(path.c_str(), &statbuf) == 0);
+}
+
+bool
+isFile(string const& path) {
+ struct stat statbuf;
+ if (::stat(path.c_str(), &statbuf) < 0) {
+ return (false);
+ }
+ return ((statbuf.st_mode & S_IFMT) == S_IFREG);
+}
+
+Umask::Umask(mode_t mask) : orig_umask_(umask(S_IWGRP | S_IWOTH)) {
+ umask(orig_umask_ | mask);
+}
+
+Umask::~Umask() {
+ umask(orig_umask_);
+}
+
+bool
+isSocket(string const& path) {
+ struct stat statbuf;
+ if (::stat(path.c_str(), &statbuf) < 0) {
+ return (false);
+ }
+ return ((statbuf.st_mode & S_IFMT) == S_IFSOCK);
+}
+
+Path::Path(string const& full_name) {
+ if (!full_name.empty()) {
+ bool dir_present = false;
+ // Find the directory.
+ size_t last_slash = full_name.find_last_of('/');
+ if (last_slash != string::npos) {
+ // Found the last slash, so extract directory component and
+ // set where the scan for the last_dot should terminate.
+ parent_path_ = full_name.substr(0, last_slash);
+ if (last_slash == full_name.size()) {
+ // The entire string was a directory, so exit and don't
+ // do any more searching.
+ return;
+ }
+
+ // Found a directory so note the fact.
+ dir_present = true;
+ }
+
+ // Now search backwards for the last ".".
+ size_t last_dot = full_name.find_last_of('.');
+ if ((last_dot == string::npos) || (dir_present && (last_dot < last_slash))) {
+ // Last "." either not found or it occurs to the left of the last
+ // slash if a directory was present (so it is part of a directory
+ // name). In this case, the remainder of the string after the slash
+ // is the name part.
+ stem_ = full_name.substr(last_slash + 1);
+ return;
+ }
+
+ // Did find a valid dot, so it and everything to the right is the
+ // extension...
+ extension_ = full_name.substr(last_dot);
+
+ // ... and the name of the file is everything in between.
+ if ((last_dot - last_slash) > 1) {
+ stem_ = full_name.substr(last_slash + 1, last_dot - last_slash - 1);
+ }
+ }
+}
+
+string
+Path::str() const {
+ return (parent_path_ + ((parent_path_.empty() || parent_path_ == "/") ? string() : "/") + stem_ + extension_);
+}
+
+string
+Path::parentPath() const {
+ return (parent_path_);
+}
+
+string
+Path::stem() const {
+ return (stem_);
+}
+
+string
+Path::extension() const {
+ return (extension_);
+}
+
+string
+Path::filename() const {
+ return (stem_ + extension_);
+}
+
+Path&
+Path::replaceExtension(string const& replacement) {
+ string const trimmed_replacement(trim(replacement));
+ if (trimmed_replacement.empty()) {
+ extension_ = string();
+ } else {
+ size_t const last_dot(trimmed_replacement.find_last_of('.'));
+ if (last_dot == string::npos) {
+ extension_ = "." + trimmed_replacement;
+ } else {
+ extension_ = trimmed_replacement.substr(last_dot);
+ }
+ }
+ return (*this);
+}
+
+Path&
+Path::replaceParentPath(string const& replacement) {
+ string const trimmed_replacement(trim(replacement));
+ if (trimmed_replacement.empty()) {
+ parent_path_ = string();
+ } else if (trimmed_replacement == "/") {
+ parent_path_ = trimmed_replacement;
+ } else if (trimmed_replacement.at(trimmed_replacement.size() - 1) == '/') {
+ parent_path_ = trimmed_replacement.substr(0, trimmed_replacement.size() - 1);
+ } else {
+ parent_path_ = trimmed_replacement;
+ }
+ return (*this);
+}
+
+TemporaryDirectory::TemporaryDirectory() {
+ char dir[]("/tmp/kea-tmpdir-XXXXXX");
+ char const* dir_name = mkdtemp(dir);
+ if (!dir_name) {
+ isc_throw(Unexpected, "mkdtemp failed " << dir << ": " << strerror(errno));
+ }
+ dir_name_ = string(dir_name);
+}
+
+TemporaryDirectory::~TemporaryDirectory() {
+ DIR *dir(opendir(dir_name_.c_str()));
+ if (!dir) {
+ return;
+ }
+
+ std::unique_ptr<DIR, void(*)(DIR*)> defer(dir, [](DIR* d) { closedir(d); });
+
+ struct dirent *i;
+ string filepath;
+ while ((i = readdir(dir))) {
+ if (strcmp(i->d_name, ".") == 0 || strcmp(i->d_name, "..") == 0) {
+ continue;
+ }
+
+ filepath = dir_name_ + '/' + i->d_name;
+ remove(filepath.c_str());
+ }
+
+ rmdir(dir_name_.c_str());
+}
+
+string TemporaryDirectory::dirName() {
+ return dir_name_;
+}
+
+std::string
+FileManager::validatePath(const std::string supported_path_str, const std::string input_path_str,
+ bool enforce_path /* = true */) {
+ // Remove the trailing "/" if it present so comparison to
+ // input's parent path functions.
+ auto supported_path_copy(supported_path_str);
+ if (supported_path_copy.back() == '/') {
+ supported_path_copy.pop_back();
+ }
+
+ Path input_path(trim(input_path_str));
+ auto filename = input_path.filename();
+ if (filename.empty()) {
+ isc_throw(BadValue, "path: '" << input_path.str() << "' has no filename");
+ }
+
+ auto parent_path = input_path.parentPath();
+ if (!parent_path.empty()) {
+ if (!enforce_path) {
+ // Security set to lax, let it fly.
+ return (input_path_str);
+ }
+
+ // We only allow absolute path equal to default. Catch an invalid path.
+ if (parent_path != supported_path_copy) {
+ isc_throw(BadValue, "invalid path specified: '"
+ << parent_path << "', supported path is '"
+ << supported_path_copy << "'");
+ }
+ }
+
+ std::string valid_path(supported_path_copy + "/" + filename);
+ return (valid_path);
+}
+
+} // namespace file
+} // namespace util
+} // namespace isc
--- /dev/null
+// Copyright (C) 2021-2025 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef KEA_UTIL_FILESYSTEM_H
+#define KEA_UTIL_FILESYSTEM_H
+
+#include <sys/stat.h>
+#include <string>
+
+namespace isc {
+namespace util {
+namespace file {
+
+/// @brief Check if there is a file or directory at the given path.
+///
+/// @param path The path being checked.
+///
+/// @return True if the path points to a file or a directory, false otherwise.
+bool
+exists(const std::string& path);
+
+/// @brief Check if there is a file at the given path.
+///
+/// @param path The path being checked.
+///
+/// @return True if the path points to a file, false otherwise including
+/// if the pointed location does not exist.
+bool
+isFile(const std::string& path);
+
+/// @brief RAII device to limit access of created files.
+struct Umask {
+ /// @brief Constructor
+ ///
+ /// Set wanted bits in umask.
+ Umask(mode_t mask);
+
+ /// @brief Destructor.
+ ///
+ /// Restore umask.
+ ~Umask();
+
+private:
+ /// @brief Original umask.
+ mode_t orig_umask_;
+};
+
+bool
+isSocket(const std::string& path);
+
+/// @brief Paths on a filesystem
+struct Path {
+ /// @brief Constructor
+ ///
+ /// Splits the full name into components.
+ Path(std::string const& path);
+
+ /// @brief Get the path in textual format.
+ ///
+ /// Counterpart for std::filesystem::path::string.
+ ///
+ /// @return stored filename.
+ std::string str() const;
+
+ /// @brief Get the parent path.
+ ///
+ /// Counterpart for std::filesystem::path::parent_path.
+ ///
+ /// @return parent path of current path.
+ std::string parentPath() const;
+
+ /// @brief Get the base name of the file without the extension.
+ ///
+ /// Counterpart for std::filesystem::path::stem.
+ ///
+ /// @return the base name of current path without the extension.
+ std::string stem() const;
+
+ /// @brief Get the extension of the file.
+ ///
+ /// Counterpart for std::filesystem::path::extension.
+ ///
+ /// @return extension of current path.
+ std::string extension() const;
+
+ /// @brief Get the name of the file, extension included.
+ ///
+ /// Counterpart for std::filesystem::path::filename.
+ ///
+ /// @return name + extension of current path.
+ std::string filename() const;
+
+ /// @brief Identifies the extension in {replacement}, trims it, and
+ /// replaces this instance's extension with it.
+ ///
+ /// Counterpart for std::filesystem::path::replace_extension.
+ ///
+ /// The change is done in the members and {this} is returned to allow call
+ /// chaining.
+ ///
+ /// @param replacement The extension to replace with.
+ ///
+ /// @return The current instance after the replacement was done.
+ Path& replaceExtension(std::string const& replacement = std::string());
+
+ /// @brief Trims {replacement} and replaces this instance's parent path with
+ /// it.
+ ///
+ /// The change is done in the members and {this} is returned to allow call
+ /// chaining.
+ ///
+ /// @param replacement The parent path to replace with.
+ ///
+ /// @return The current instance after the replacement was done.
+ Path& replaceParentPath(std::string const& replacement = std::string());
+
+private:
+ /// @brief Parent path.
+ std::string parent_path_;
+
+ /// @brief Stem.
+ std::string stem_;
+
+ /// @brief File name extension.
+ std::string extension_;
+};
+
+struct TemporaryDirectory {
+ TemporaryDirectory();
+ ~TemporaryDirectory();
+ std::string dirName();
+private:
+ std::string dir_name_;
+};
+
+/// @brief Class that provides basic file related tasks.
+class FileManager {
+public:
+ /// @brief Validates a file path against a supported path.
+ ///
+ /// If the input path specifies a parent path and file name, the parent path
+ /// is validated against the supported path. If they match, the function returns
+ /// the validated path. If the input path contains only a file name the function
+ /// returns valid path using the supported path and the input path name.
+ ///
+ /// @param supported_path_str absolute path specifying the supported path
+ /// of the file against which the input path is validated.
+ /// @param input_path_str file path to validate.
+ /// @param enforce_path enables validation against the supported path. If false
+ /// verifies only that the path contains a file name.
+ ///
+ /// @return validated path as a string (supported path + input file name)
+ ///
+ /// @throw BadValue if the input path does not include a file name or if the
+ /// it the parent path does not path the supported path.
+ static std::string validatePath(const std::string supported_path_str,
+ const std::string input_path_str,
+ bool enforce_path = true);
+};
+
+} // namespace file
+} // namespace util
+} // namespace isc
+
+#endif // KEA_UTIL_FILESYSTEM_H
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DABS_SRCDIR=\"$(abs_srcdir)\"
AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_builddir)\"
# XXX: we'll pollute the top builddir for creating a temporary test file
# used to bind a UNIX domain socket so we can minimize the risk of exceeding
run_unittests_SOURCES += versioned_csv_file_unittest.cc
run_unittests_SOURCES += watch_socket_unittests.cc
run_unittests_SOURCES += watched_thread_unittest.cc
+run_unittests_SOURCES += filesystem_unittests.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
--- /dev/null
+// Copyright (C) 2015-2024 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <exceptions/exceptions.h>
+#include <testutils/gtest_utils.h>
+#include <util/filesystem.h>
+
+#include <fstream>
+#include <string>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::util::file;
+using namespace std;
+
+namespace {
+
+/// @brief Check that the components are split correctly.
+TEST(PathTest, components) {
+ // Complete name
+ Path fname("/alpha/beta/gamma.delta");
+ EXPECT_EQ("/alpha/beta/gamma.delta", fname.str());
+ EXPECT_EQ("/alpha/beta", fname.parentPath());
+ EXPECT_EQ("gamma", fname.stem());
+ EXPECT_EQ(".delta", fname.extension());
+ EXPECT_EQ("gamma.delta", fname.filename());
+}
+
+/// @brief Check replaceExtension.
+TEST(PathTest, replaceExtension) {
+ Path fname("a.b");
+ EXPECT_EQ("a.b", fname.str());
+
+ EXPECT_EQ("a", fname.replaceExtension("").str());
+ EXPECT_EQ("a.f", fname.replaceExtension(".f").str());
+ EXPECT_EQ("a.f", fname.replaceExtension("f").str());
+ EXPECT_EQ("a./c/d/", fname.replaceExtension(" /c/d/ ").str());
+ EXPECT_EQ("a.f", fname.replaceExtension("/c/d/e.f").str());
+ EXPECT_EQ("a.f", fname.replaceExtension("e.f").str());
+}
+
+/// @brief Check replaceParentPath.
+TEST(PathTest, replaceParentPath) {
+ Path fname("a.b");
+ EXPECT_EQ("", fname.parentPath());
+ EXPECT_EQ("a.b", fname.str());
+
+ fname.replaceParentPath("/just/some/dir/");
+ EXPECT_EQ("/just/some/dir", fname.parentPath());
+ EXPECT_EQ("/just/some/dir/a.b", fname.str());
+
+ fname.replaceParentPath("/just/some/dir");
+ EXPECT_EQ("/just/some/dir", fname.parentPath());
+ EXPECT_EQ("/just/some/dir/a.b", fname.str());
+
+ fname.replaceParentPath("/");
+ EXPECT_EQ("/", fname.parentPath());
+ EXPECT_EQ("/a.b", fname.str());
+
+ fname.replaceParentPath("");
+ EXPECT_EQ("", fname.parentPath());
+ EXPECT_EQ("a.b", fname.str());
+
+ fname = Path("/first/a.b");
+ EXPECT_EQ("/first", fname.parentPath());
+ EXPECT_EQ("/first/a.b", fname.str());
+
+ fname.replaceParentPath("/just/some/dir");
+ EXPECT_EQ("/just/some/dir", fname.parentPath());
+ EXPECT_EQ("/just/some/dir/a.b", fname.str());
+}
+
+} // namespace