]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
backport #3831 to 2_6
authorRazvan Becheriu <razvan@isc.org>
Fri, 16 May 2025 16:30:02 +0000 (19:30 +0300)
committerRazvan Becheriu <razvan@isc.org>
Fri, 16 May 2025 16:30:02 +0000 (19:30 +0300)
21 files changed:
doc/sphinx/arm/ctrl-channel.rst
src/bin/agent/tests/ca_controller_unittests.cc
src/bin/d2/tests/d2_command_unittest.cc
src/bin/dhcp4/ctrl_dhcp4_srv.cc
src/bin/dhcp4/ctrl_dhcp4_srv.h
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
src/bin/dhcp6/ctrl_dhcp6_srv.cc
src/bin/dhcp6/ctrl_dhcp6_srv.h
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
src/hooks/dhcp/lease_cmds/tests/lease_cmds4_unittest.cc
src/hooks/dhcp/lease_cmds/tests/lease_cmds6_unittest.cc
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
src/lib/process/d_controller.cc
src/lib/process/d_controller.h
src/lib/process/daemon.cc
src/lib/process/daemon.h
src/lib/process/tests/daemon_unittest.cc
src/lib/util/filesystem.cc
src/lib/util/filesystem.h
src/lib/util/tests/filesystem_unittests.cc

index 227ad94420e8a89e22fbc2aa34df6c65330a791c..c649cf541b2e3bcde8ab0967b7adb475559dfe73 100644 (file)
@@ -511,6 +511,12 @@ An example command invocation looks like this:
        }
    }
 
+.. note::
+
+    As of Kea 2.6.3, the config file file may only be written to the same
+    directory as the config file used when starting Kea (passed as a ``-c``
+    argument).
+
 .. isccmd:: leases-reclaim
 .. _command-leases-reclaim:
 
index 9a18c27a0a0fbb671dec336fd0f9d7c40c98e820..44f2b2e4a2e1c1aa1f99bc30d99a2a50b329065c 100644 (file)
@@ -515,6 +515,9 @@ TEST_F(CtrlAgentControllerTest, configWrite) {
     // Now clean up after ourselves.
     ctrl->registerCommands();
 
+    // Add a config file.
+    ctrl->setConfigFile(string(TEST_DATA_BUILDDIR) + string("/config.json"));
+
     // First, build the command:
     string file = string(TEST_DATA_BUILDDIR) + string("/config-write.json");
     string cmd_txt = "{ \"command\": \"config-write\" }";
@@ -550,6 +553,54 @@ TEST_F(CtrlAgentControllerTest, configWrite) {
     ctrl->deregisterCommands();
 }
 
+// Tests that config-write fails with a bad path.
+TEST_F(CtrlAgentControllerTest, badConfigWrite) {
+    ASSERT_NO_THROW(initProcess());
+    EXPECT_TRUE(checkProcess());
+
+    // The framework available makes it very difficult to test the actual
+    // code as CtrlAgentController is not initialized the same way it is
+    // in production code. In particular, the way CtrlAgentController
+    // is initialized in tests does not call registerCommands().
+    // This is a crude workaround for this problem. Proper solution should
+    // be developed sooner rather than later.
+    const DControllerBasePtr& base = getController();
+    const CtrlAgentControllerPtr& ctrl
+        = boost::dynamic_pointer_cast<CtrlAgentController>(base);
+    ASSERT_TRUE(ctrl);
+    // Now clean up after ourselves.
+    ctrl->registerCommands();
+
+    // Add a config file.
+    ctrl->setConfigFile(string(TEST_DATA_BUILDDIR) + string("/config.json"));
+
+    // First, build the command:
+    string file("/tmp/config-write.json");
+    string cmd_txt = "{ \"command\": \"config-write\" }";
+    ConstElementPtr cmd = Element::fromJSON(cmd_txt);
+    ConstElementPtr params = Element::fromJSON("{\"filename\": \"" + file + "\" }");
+    CtrlAgentCommandMgr& mgr_ =  CtrlAgentCommandMgr::instance();
+
+    // Send the command
+    ConstElementPtr answer = mgr_.handleCommand("config-write", params, cmd);
+
+    // Check that the command failed.
+    string expected = "not allowed to write config into ";
+    expected += file;
+    expected += ": file ";
+    expected += file;
+    expected += " must be in the same directory as the config file (";
+    expected += string(TEST_DATA_BUILDDIR) + string("/config.json");
+    expected += ")";
+    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR, expected);
+
+    // Remove the file.
+    ::remove(file.c_str());
+
+    // Now clean up after ourselves.
+    ctrl->deregisterCommands();
+}
+
 // Tests if config-reload attempts to reload a file and reports that the
 // file is missing.
 TEST_F(CtrlAgentControllerTest, configReloadMissingFile) {
index 8b133b174ae3d3f0702e2a299077c9c4930aa527..1f3a84bd3375433be5ac5466dad36a5858040b1b 100644 (file)
@@ -1036,6 +1036,9 @@ TEST_F(CtrlChannelD2Test, writeConfigFilename) {
     EXPECT_NO_THROW(createUnixChannelServer());
     string response;
 
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
     sendUnixCommand("{ \"command\": \"config-write\", "
                     "\"arguments\": { \"filename\": \"test2.json\" } }",
                     response);
@@ -1043,6 +1046,60 @@ TEST_F(CtrlChannelD2Test, writeConfigFilename) {
     ::remove("test2.json");
 }
 
+// Tests if config-write can be called with a valid full path as parameter.
+TEST_F(CtrlChannelD2Test, configWriteFullPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("/tmp/test1.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/test2.json\" } }",
+                    response);
+
+    checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "/tmp/test2.json");
+    ::remove("/tmp/test2.json");
+}
+
+// Tests if config-write raises an error with invalid path as parameter.
+TEST_F(CtrlChannelD2Test, configWriteBadPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/test2.json\" } }",
+                    response);
+
+    string expected = "not allowed to write config into /tmp/test2.json: ";
+    expected += "file /tmp/test2.json must be in the same directory ";
+    expected += "as the config file (test1.json)";
+    checkConfigWrite(response, CONTROL_RESULT_ERROR, expected);
+    ::remove("/tmp/test2.json");
+}
+
+// Tests if config-write raises an error with invalid full path as parameter.
+TEST_F(CtrlChannelD2Test, configWriteBadFullPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("/tmp/kea1/test.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/kea2/test.json\" } }",
+                    response);
+
+    string expected = "not allowed to write config into /tmp/kea2/test.json: ";
+    expected += "file /tmp/kea2/test.json must be in the same directory ";
+    expected += "as the config file (/tmp/kea1/test.json)";
+    checkConfigWrite(response, CONTROL_RESULT_ERROR, expected);
+    ::remove("/tmp/kea2/test.json");
+}
+
 // Tests if config-reload attempts to reload a file and reports that the
 // file is missing.
 TEST_F(CtrlChannelD2Test, configReloadMissingFile) {
index 1374719d9c90e61a1482afeca211337522d2a8c6..d7c0b93147b3e804dcf7d0216da8f0d9aac2dcee 100644 (file)
@@ -288,11 +288,19 @@ ControlledDhcpv4Srv::commandConfigWriteHandler(const string&,
         // filename parameter was not specified, so let's use whatever we remember
         // from the command-line
         filename = getConfigFile();
-    }
-
-    if (filename.empty()) {
-        return (createAnswer(CONTROL_RESULT_ERROR, "Unable to determine filename."
-                             "Please specify filename explicitly."));
+        if (filename.empty()) {
+            return (createAnswer(CONTROL_RESULT_ERROR, "Unable to determine filename."
+                                 "Please specify filename explicitly."));
+        }
+    } else {
+        try {
+            checkWriteConfigFile(filename);
+        } catch (const isc::Exception& ex) {
+            std::ostringstream msg;
+            msg << "not allowed to write config into " << filename
+                << ": " << ex.what();
+            return (createAnswer(CONTROL_RESULT_ERROR, msg.str()));
+        }
     }
 
     // Ok, it's time to write the file.
index 1c8e6dc8adbe8bbb42e20c97e881ef54f05684f9..a16e53fbb2ba963f1d9bbb579be2fe950fc768a1 100644 (file)
@@ -179,9 +179,9 @@ private:
     /// current configuration to disk. This command takes one optional
     /// parameter called filename. If specified, the current configuration
     /// will be written to that file. If not specified, the file used during
-    /// Kea start-up will be used. To avoid any exploits, the path is
-    /// always relative and .. is not allowed in the filename. This is
-    /// a security measure against exploiting file writes remotely.
+    /// Kea start-up will be used. To avoid any exploits, the target
+    /// directory must be the same as a security measure against
+    /// exploiting file writes remotely.
     ///
     /// @param command (ignored)
     /// @param args may contain optional string argument filename
index f4960588effd73ff92058d455b397a63eb0c31ad..d99b7b3233aa2cc01e905350736491fd1f65f0d7 100644 (file)
@@ -1471,6 +1471,9 @@ TEST_F(CtrlChannelDhcpv4SrvTest, configWriteFilename) {
     createUnixChannelServer();
     std::string response;
 
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
     sendUnixCommand("{ \"command\": \"config-write\", "
                     "\"arguments\": { \"filename\": \"test2.json\" } }", response);
 
@@ -1478,6 +1481,57 @@ TEST_F(CtrlChannelDhcpv4SrvTest, configWriteFilename) {
     ::remove("test2.json");
 }
 
+// Tests if config-write can be called with a valid full path as parameter.
+TEST_F(CtrlChannelDhcpv4SrvTest, configWriteFullPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("/tmp/test1.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/test2.json\" } }", response);
+
+    checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "/tmp/test2.json");
+    ::remove("/tmp/test2.json");
+}
+
+// Tests if config-write raises an error with invalid path as parameter.
+TEST_F(CtrlChannelDhcpv4SrvTest, configWriteBadPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/test2.json\" } }", response);
+
+    string expected = "not allowed to write config into /tmp/test2.json: ";
+    expected += "file /tmp/test2.json must be in the same directory ";
+    expected += "as the config file (test1.json)";
+    checkConfigWrite(response, CONTROL_RESULT_ERROR, expected);
+    ::remove("/tmp/test2.json");
+}
+
+// Tests if config-write raises an error with invalid full path as parameter.
+TEST_F(CtrlChannelDhcpv4SrvTest, configWriteBadFullPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("/tmp/kea1/test.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/kea2/test.json\" } }", response);
+
+    string expected = "not allowed to write config into /tmp/kea2/test.json: ";
+    expected += "file /tmp/kea2/test.json must be in the same directory ";
+    expected += "as the config file (/tmp/kea1/test.json)";
+    checkConfigWrite(response, CONTROL_RESULT_ERROR, expected);
+    ::remove("/tmp/kea2/test.json");
+}
+
 // Tests if config-reload attempts to reload a file and reports that the
 // file is missing.
 TEST_F(CtrlChannelDhcpv4SrvTest, configReloadMissingFile) {
index 5792653d3b8e12c59fbbb116e8ead7adf5667c5d..868e251c78476f432e65829f9646272fd5ec8ddb 100644 (file)
@@ -291,11 +291,19 @@ ControlledDhcpv6Srv::commandConfigWriteHandler(const string&,
         // filename parameter was not specified, so let's use whatever we remember
         // from the command-line
         filename = getConfigFile();
-    }
-
-    if (filename.empty()) {
-        return (createAnswer(CONTROL_RESULT_ERROR, "Unable to determine filename."
-                             "Please specify filename explicitly."));
+        if (filename.empty()) {
+            return (createAnswer(CONTROL_RESULT_ERROR, "Unable to determine filename."
+                                 "Please specify filename explicitly."));
+        }
+    } else {
+        try {
+            checkWriteConfigFile(filename);
+        } catch (const isc::Exception& ex) {
+            std::ostringstream msg;
+            msg << "not allowed to write config into " << filename
+                << ": " << ex.what();
+            return (createAnswer(CONTROL_RESULT_ERROR, msg.str()));
+        }
     }
 
     // Ok, it's time to write the file.
index 3d75bae6f9d1a00e6e5985cf776472bc9d6d0644..77d8f450ca4f41e70d3e252c06a641364eda5ae3 100644 (file)
@@ -179,9 +179,9 @@ private:
     /// current configuration to disk. This command takes one optional
     /// parameter called filename. If specified, the current configuration
     /// will be written to that file. If not specified, the file used during
-    /// Kea start-up will be used. To avoid any exploits, the path is
-    /// always relative and .. is not allowed in the filename. This is
-    /// a security measure against exploiting file writes remotely.
+    /// Kea start-up will be used. To avoid any exploits, the target
+    /// directory must be the same as a security measure against
+    /// exploiting file writes remotely.
     ///
     /// @param command (ignored)
     /// @param args may contain optional string argument filename
index 87bee8e493c0e0c1401de90f7bf932171a6f627f..21f85020625cf1c8f47eae6cc048bf8c29d8ce5e 100644 (file)
@@ -1491,6 +1491,9 @@ TEST_F(CtrlChannelDhcpv6SrvTest, configWriteFilename) {
     createUnixChannelServer();
     std::string response;
 
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
     sendUnixCommand("{ \"command\": \"config-write\", "
                     "\"arguments\": { \"filename\": \"test2.json\" } }", response);
 
@@ -1498,6 +1501,57 @@ TEST_F(CtrlChannelDhcpv6SrvTest, configWriteFilename) {
     ::remove("test2.json");
 }
 
+// Tests if config-write can be called with a valid full path as parameter.
+TEST_F(CtrlChannelDhcpv6SrvTest, configWriteFullPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("/tmp/test1.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/test2.json\" } }", response);
+
+    checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "/tmp/test2.json");
+    ::remove("/tmp/test2.json");
+}
+
+// Tests if config-write raises an error with invalid path as parameter.
+TEST_F(CtrlChannelDhcpv6SrvTest, configWriteBadPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/test2.json\" } }", response);
+
+    string expected = "not allowed to write config into /tmp/test2.json: ";
+    expected += "file /tmp/test2.json must be in the same directory ";
+    expected += "as the config file (test1.json)";
+    checkConfigWrite(response, CONTROL_RESULT_ERROR, expected);
+    ::remove("/tmp/test2.json");
+}
+
+// Tests if config-write raises an error with invalid full path as parameter.
+TEST_F(CtrlChannelDhcpv6SrvTest, configWriteBadFullPath) {
+    createUnixChannelServer();
+    std::string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("/tmp/kea1/test.json");
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"/tmp/kea2/test.json\" } }", response);
+
+    string expected = "not allowed to write config into /tmp/kea2/test.json: ";
+    expected += "file /tmp/kea2/test.json must be in the same directory ";
+    expected += "as the config file (/tmp/kea1/test.json)";
+    checkConfigWrite(response, CONTROL_RESULT_ERROR, expected);
+    ::remove("/tmp/kea2/test.json");
+}
+
 // Tests if config-reload attempts to reload a file and reports that the
 // file is missing.
 TEST_F(CtrlChannelDhcpv6SrvTest, configReloadMissingFile) {
index 825390e16310ea29c00de4d69618f4b3138827da..e201abdbed0bf62c5472d8b3a5e3c7e6929e6b06 100644 (file)
@@ -3437,13 +3437,13 @@ void Lease4CmdsTest::testLease4Write() {
         "{\n"
         "    \"command\": \"lease4-write\",\n"
         "    \"arguments\": {"
-        "        \"filename\": \"/tmp/myleases.txt\"\n"
+        "        \"filename\": \"/foo-bar/myleases.txt\"\n"
         "    }\n"
         "}";
 
     std::ostringstream os;
     os << "'filename' parameter is invalid: invalid path specified:"
-       << " '/tmp', supported path is '" << CfgMgr::instance().getDataDir() << "'";
+       << " '/foo-bar', supported path is '" << CfgMgr::instance().getDataDir() << "'";
 
     testCommand(txt, CONTROL_RESULT_ERROR, os.str());
 }
index 2fb2d796a254bf2e3e34866ab25014da17fff98c..ea281b30b8401619a8a358f9c9066c815b864e7d 100644 (file)
@@ -4172,13 +4172,13 @@ void Lease6CmdsTest::testLease6Write() {
         "{\n"
         "    \"command\": \"lease6-write\",\n"
         "    \"arguments\": {"
-        "        \"filename\": \"/tmp/myleases.txt\"\n"
+        "        \"filename\": \"/foo-bar/myleases.txt\"\n"
         "    }\n"
         "}";
 
     std::ostringstream os;
     os << "'filename' parameter is invalid: invalid path specified:"
-       << " '/tmp', supported path is '" << CfgMgr::instance().getDataDir() << "'";
+       << " '/foo-bar', supported path is '" << CfgMgr::instance().getDataDir() << "'";
 }
 
 TEST_F(Lease6CmdsTest, lease6AddMissingParams) {
index 860196ccef66824553d84f5c3c428fd761c3d98e..8dd9508526f5759eb76a50ca92cf904c48338553 100644 (file)
@@ -279,6 +279,7 @@ public:
     }
 
     void clear() {
+        data_dir_env_var_.setValue();
         CfgMgr::instance().setFamily(AF_INET);
         resetDataDir();
         CfgMgr::instance().clear();
index 8bc79f370c7758ecb1b42e0e1cc936de9658aa82..94b5cf7f20d31aa277aac9c9eeaa860a58fe8d40 100644 (file)
@@ -116,6 +116,9 @@ public:
         extra_files_(),
         data_dir_env_var_("KEA_DHCP_DATA_DIR") {
 
+        // Reset the env variable.
+        data_dir_env_var_.setValue();
+
         // Save the pre-test data dir and set it to the test directory.
         CfgMgr::instance().clear();
         original_datadir_ = CfgMgr::instance().getDataDir();
index b5f7e14184a48e22fd861a03ffc6da43f61b932e..214c68f4d7c85abe96891ac45127a3344cf08226 100644 (file)
@@ -508,13 +508,22 @@ DControllerBase::configWriteHandler(const std::string&,
                                  "Unable to determine filename."
                                  "Please specify filename explicitly."));
         }
+    } else {
+        try {
+            checkWriteConfigFile(filename);
+        } catch (const isc::Exception& ex) {
+            std::ostringstream msg;
+            msg << "not allowed to write config into " << filename
+                << ": " << ex.what();
+            return (createAnswer(CONTROL_RESULT_ERROR, msg.str()));
+        }
     }
 
     // Ok, it's time to write the file.
     size_t size = 0;
-    ElementPtr cfg = process_->getCfgMgr()->getContext()->toElement();
 
     try {
+        ElementPtr cfg = process_->getCfgMgr()->getContext()->toElement();
         size = writeConfigFile(filename, cfg);
     } catch (const isc::Exception& ex) {
         return (createAnswer(CONTROL_RESULT_ERROR,
index 91b777b786a9cc91ba73828050742af0ce115cbf..417e21e1491aae006adaac505da9d3f0078904e6 100644 (file)
@@ -283,9 +283,9 @@ public:
     /// current configuration to disk. This command takes one optional
     /// parameter called filename. If specified, the current configuration
     /// will be written to that file. If not specified, the file used during
-    /// Kea start-up will be used. To avoid any exploits, the path is
-    /// always relative and .. is not allowed in the filename. This is
-    /// a security measure against exploiting file writes remotely.
+    /// Kea start-up will be used. To avoid any exploits, the target
+    /// directory must be the same as a security measure against
+    /// exploiting file writes remotely.
     ///
     /// @param command (ignored)
     /// @param args may contain optional string argument filename
index e201f5e8a5549f41ff6aa9907a7c03eb3aeebed4..3f9aae03347be21e0017c0934542a307b6aa6019 100644 (file)
@@ -125,6 +125,28 @@ Daemon::checkConfigFile() const {
     }
 }
 
+void
+Daemon::checkWriteConfigFile(std::string& file) {
+    Path path(file);
+    // from checkConfigFile().
+    if (path.stem().empty()) {
+        isc_throw(isc::BadValue, "config file:" << file
+                  << " is missing file name");
+    }
+    Path current(config_file_);
+    if (current.parentDirectory() == path.parentDirectory()) {
+        // Same parent directories!
+        return;
+    }
+    if (path.parentDirectory().empty()) {
+        // Note the current parent directory can't be empty here.
+        file = current.parentDirectory() + file;
+        return;
+    }
+    isc_throw(isc::BadValue, "file " << file << " must be in the same "
+              << "directory as the config file (" << config_file_ << ")");
+}
+
 std::string
 Daemon::getProcName() {
     return (proc_name_);
index d64445a77c5a1a775e097fde2f136a30d1055276..83db87e9af75e2258757e3a724aa926324613d2d 100644 (file)
@@ -139,6 +139,15 @@ public:
     /// @throw BadValue when the configuration file name is bad.
     void checkConfigFile() const;
 
+    /// @brief Checks the to-be-written configuration file name.
+    ///
+    /// @note As a side effect prepend the current config file path
+    /// when the name does not contain a slash.
+    ///
+    /// @param[in][out] file Reference to the TBW configuration file name.
+    /// @throw BadValue when not in the same directory.
+    void checkWriteConfigFile(std::string& file);
+
     /// @brief Writes current configuration to specified file
     ///
     /// This method writes the current configuration to specified file.
index ca0bcd1f317e606c7792cb420880a33700eb7ece..038f6f4905fa724e6e29e02e3191a0841df1a690 100644 (file)
@@ -40,8 +40,8 @@ std::string DaemonImpl::getVersion(bool extended) {
     }
 }
 
-};
-};
+}
+}
 
 namespace {
 
@@ -77,7 +77,6 @@ private:
     std::string env_copy_;
 };
 
-
 // Very simple test. Checks whether Daemon can be instantiated and its
 // default parameters are sane
 TEST_F(DaemonTest, constructor) {
@@ -113,6 +112,47 @@ TEST_F(DaemonTest, checkConfigFile) {
     EXPECT_NO_THROW(instance.checkConfigFile());
 }
 
+// Verify write config file checker.
+TEST_F(DaemonTest, checkWriteConfigFile) {
+    Daemon instance;
+
+    std::string file("");
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "/tmp/";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "tmp/";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    instance.setConfigFile("/tmp/foo");
+    file = "/foo/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "/tmp/foo/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "/tmp/bar";
+    EXPECT_NO_THROW(instance.checkWriteConfigFile(file));
+    EXPECT_EQ("/tmp/bar", file);
+    file = "bar";
+    EXPECT_NO_THROW(instance.checkWriteConfigFile(file));
+    EXPECT_EQ("/tmp/bar", file);
+    instance.setConfigFile("tmp/foo");
+    file = "/tmp/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "/tmp/foo/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "tmp/bar";
+    EXPECT_NO_THROW(instance.checkWriteConfigFile(file));
+    EXPECT_EQ("tmp/bar", file);
+    instance.setConfigFile("foo");
+    file = "/tmp/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "tmp/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "foo/bar";
+    EXPECT_THROW(instance.checkWriteConfigFile(file), BadValue);
+    file = "bar";
+    EXPECT_NO_THROW(instance.checkWriteConfigFile(file));
+    EXPECT_EQ("bar", file);
+}
+
 // Verify process name accessors
 TEST_F(DaemonTest, getSetProcName) {
     Daemon instance;
@@ -318,7 +358,6 @@ TEST_F(DaemonTest, exitValue) {
     EXPECT_EQ(77, instance.getExitValue());
 }
 
-
 // More tests will appear here as we develop Daemon class.
 
-};
+}
index 495091dad29d8e5e3b3f92ee885e5746d2bc5904..3b01ee644593aeb7f4971ff221ed0f4a43677ccd 100644 (file)
@@ -81,11 +81,14 @@ setUmask() {
 }
 
 Path::Path(string const& full_name) {
+    dir_present_ = false;
     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 a directory so note the fact.
+            dir_present_ = true;
+
             // 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);
@@ -94,14 +97,11 @@ Path::Path(string const& full_name) {
                 // 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))) {
+        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
@@ -123,7 +123,7 @@ Path::Path(string const& full_name) {
 
 string
 Path::str() const {
-    return (parent_path_ + ((parent_path_.empty() || parent_path_ == "/") ? string() : "/") + stem_ + extension_);
+    return (parent_path_ + (dir_present_ ? "/" : "") + stem_ + extension_);
 }
 
 string
@@ -131,6 +131,11 @@ Path::parentPath() const {
     return (parent_path_);
 }
 
+string
+Path::parentDirectory() const {
+    return (parent_path_ + (dir_present_ ? "/" : ""));
+}
+
 string
 Path::stem() const {
     return (stem_);
@@ -165,10 +170,9 @@ Path::replaceExtension(string const& replacement) {
 Path&
 Path::replaceParentPath(string const& replacement) {
     string const trimmed_replacement(trim(replacement));
-    if (trimmed_replacement.empty()) {
+    dir_present_ = (trimmed_replacement.find_last_of('/') != string::npos);
+    if (trimmed_replacement.empty() || (trimmed_replacement == "/")) {
         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 {
index eaf2701b08b5fa0dbf92d0559bc67b2724b05b3e..951dd622db02b91a1c9d0b18eebcac0b2c9b4430 100644 (file)
@@ -75,6 +75,14 @@ struct Path {
     /// @return parent path of current path.
     std::string parentPath() const;
 
+    /// @brief Get the parent directory.
+    ///
+    /// Empty if no directory is present, the parent path follwed by
+    /// a slash otherwise.
+    ///
+    /// @return parent directory of current path.
+    std::string parentDirectory() const;
+
     /// @brief Get the base name of the file without the extension.
     ///
     /// Counterpart for std::filesystem::path::stem.
@@ -121,6 +129,9 @@ struct Path {
     Path& replaceParentPath(std::string const& replacement = std::string());
 
 private:
+    /// @brief Is a directory present.
+    bool dir_present_;
+
     /// @brief Parent path.
     std::string parent_path_;
 
index b246f5bdb3e24066b3481eb6a5cd4612f87ee6b2..40723726c16f4f295ebfdb430be4a2b6430675dc 100644 (file)
@@ -109,9 +109,55 @@ TEST(PathTest, components) {
     Path fname("/alpha/beta/gamma.delta");
     EXPECT_EQ("/alpha/beta/gamma.delta", fname.str());
     EXPECT_EQ("/alpha/beta", fname.parentPath());
+    EXPECT_EQ("/alpha/beta/", fname.parentDirectory());
     EXPECT_EQ("gamma", fname.stem());
     EXPECT_EQ(".delta", fname.extension());
     EXPECT_EQ("gamma.delta", fname.filename());
+
+    // The root.
+    Path root("/");
+    EXPECT_EQ("/", root.str());
+    EXPECT_EQ("", root.parentPath());
+    EXPECT_EQ("/", root.parentDirectory());
+    EXPECT_EQ("", root.stem());
+    EXPECT_EQ("", root.extension());
+    EXPECT_EQ("", root.filename());
+
+    // In the root directory.
+    Path inroot("/gamma.delta");
+    EXPECT_EQ("/gamma.delta", inroot.str());
+    EXPECT_EQ("", inroot.parentPath());
+    EXPECT_EQ("/", inroot.parentDirectory());
+    EXPECT_EQ("gamma", inroot.stem());
+    EXPECT_EQ(".delta", inroot.extension());
+    EXPECT_EQ("gamma.delta", inroot.filename());
+
+    // No directory.
+    Path nodir("gamma.delta");
+    EXPECT_EQ("gamma.delta", nodir.str());
+    EXPECT_EQ("", nodir.parentPath());
+    EXPECT_EQ("", nodir.parentDirectory());
+    EXPECT_EQ("gamma", nodir.stem());
+    EXPECT_EQ(".delta", nodir.extension());
+    EXPECT_EQ("gamma.delta", nodir.filename());
+
+    // Relative name.
+    Path relative("../alpha/gamma.delta");
+    EXPECT_EQ("../alpha/gamma.delta", relative.str());
+    EXPECT_EQ("../alpha", relative.parentPath());
+    EXPECT_EQ("../alpha/", relative.parentDirectory());
+    EXPECT_EQ("gamma", relative.stem());
+    EXPECT_EQ(".delta", relative.extension());
+    EXPECT_EQ("gamma.delta", relative.filename());
+
+    // Multiple extensions.
+    Path extensions("/alpha/beta/gamma.delta.epsilon");
+    EXPECT_EQ("/alpha/beta/gamma.delta.epsilon", extensions.str());
+    EXPECT_EQ("/alpha/beta", extensions.parentPath());
+    EXPECT_EQ("/alpha/beta/", extensions.parentDirectory());
+    EXPECT_EQ("gamma.delta", extensions.stem());
+    EXPECT_EQ(".epsilon", extensions.extension());
+    EXPECT_EQ("gamma.delta.epsilon", extensions.filename());
 }
 
 /// @brief Check replaceExtension.
@@ -131,30 +177,37 @@ TEST(PathTest, replaceExtension) {
 TEST(PathTest, replaceParentPath) {
     Path fname("a.b");
     EXPECT_EQ("", fname.parentPath());
+    EXPECT_EQ("", fname.parentDirectory());
     EXPECT_EQ("a.b", fname.str());
 
     fname.replaceParentPath("/just/some/dir/");
     EXPECT_EQ("/just/some/dir", fname.parentPath());
+    EXPECT_EQ("/just/some/dir/", fname.parentDirectory());
     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/", fname.parentDirectory());
     EXPECT_EQ("/just/some/dir/a.b", fname.str());
 
     fname.replaceParentPath("/");
-    EXPECT_EQ("/", fname.parentPath());
+    EXPECT_EQ("", fname.parentPath());
+    EXPECT_EQ("/", fname.parentDirectory());
     EXPECT_EQ("/a.b", fname.str());
 
     fname.replaceParentPath("");
     EXPECT_EQ("", fname.parentPath());
+    EXPECT_EQ("", fname.parentDirectory());
     EXPECT_EQ("a.b", fname.str());
 
     fname = Path("/first/a.b");
     EXPECT_EQ("/first", fname.parentPath());
+    EXPECT_EQ("/first/", fname.parentDirectory());
     EXPECT_EQ("/first/a.b", fname.str());
 
     fname.replaceParentPath("/just/some/dir");
     EXPECT_EQ("/just/some/dir", fname.parentPath());
+    EXPECT_EQ("/just/some/dir/", fname.parentDirectory());
     EXPECT_EQ("/just/some/dir/a.b", fname.str());
 }