]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3210] refactor file utilities
authorAndrei Pavel <andrei@isc.org>
Mon, 4 Mar 2024 09:48:36 +0000 (11:48 +0200)
committerAndrei Pavel <andrei@isc.org>
Thu, 21 Mar 2024 16:30:04 +0000 (18:30 +0200)
- Unify filename.h and file_utilities.h under filesystem.h.
- Rename Filename class to Path since it more accurately represents
  it.
- Make its interface as close as possible to that of std::filesystem::path.
- Remove unused method expandWithDefault.
- Rename useAsDefault to replaceExtension since it was only used in the
  purpose of replacing extension.
- Unroll the split method in the constructor.

19 files changed:
src/bin/keactrl/keactrl.in
src/hooks/dhcp/high_availability/ha_config_parser.cc
src/lib/asiolink/common_tls.cc
src/lib/http/basic_auth_config.cc
src/lib/http/tests/tls_server_unittests.cc
src/lib/log/compiler/message.cc
src/lib/mysql/mysql_connection.cc
src/lib/pgsql/pgsql_connection.cc
src/lib/process/daemon.cc
src/lib/util/Makefile.am
src/lib/util/file_utilities.cc [deleted file]
src/lib/util/file_utilities.h [deleted file]
src/lib/util/filename.cc [deleted file]
src/lib/util/filename.h [deleted file]
src/lib/util/filesystem.cc [new file with mode: 0644]
src/lib/util/filesystem.h [new file with mode: 0644]
src/lib/util/tests/file_utilities_unittest.cc [deleted file]
src/lib/util/tests/filename_unittest.cc [deleted file]
src/lib/util/tests/filesystem_unittests.cc [new file with mode: 0644]

index 7ad71e366888f71b563a12226c02f60659f89b60..22a3f9af4a7e5f2f75c017ce2a664315866a5792 100644 (file)
@@ -109,8 +109,7 @@ get_pid_from_file() {
         ;;
     esac
 
-    # Extract the name portion (from last slash to last dot) of the config file name
-    # File name and extension are documented in src/lib/util/filename.h
+    # Extract the name portion (from last slash to last dot) of the config file name.
     local conf_name
     conf_name=$(basename -- "${kea_config_file}" | rev | cut -f2- -d'.' | rev)
 
index a1faae6ea845518939bf1a649c34aeb1be7edbc7..18747ae964a1e819259a11d44e819fc74d818131 100644 (file)
@@ -10,7 +10,7 @@
 #include <ha_log.h>
 #include <ha_service_states.h>
 #include <cc/dhcp_config_error.h>
-#include <util/file_utilities.h>
+#include <util/filesystem.h>
 #include <boost/make_shared.hpp>
 #include <limits>
 #include <set>
index 35ca637bdc41f5909027a73ffe15d56a9adf0158..4363be4878d3c77aaf6a5eb598c3a4fee1aecba0 100644 (file)
@@ -10,7 +10,7 @@
 
 #include <asiolink/asio_wrapper.h>
 #include <asiolink/crypto_tls.h>
-#include <util/file_utilities.h>
+#include <util/filesystem.h>
 
 using namespace isc::cryptolink;
 using namespace isc::util;
index 3d96b98a625ce02121f7551b7dfa60c67303c512..1e6a727094361b2999d3bb32e55c672db19d958f 100644 (file)
@@ -8,7 +8,7 @@
 
 #include <http/auth_log.h>
 #include <http/basic_auth_config.h>
-#include <util/file_utilities.h>
+#include <util/filesystem.h>
 #include <util/strutil.h>
 
 using namespace isc;
index 29e50c1de511153edb58bf883454f0a2820aaf5d..cca66ac8f145c5d05386356531f3f7361fe7a90b 100644 (file)
@@ -40,7 +40,6 @@ using namespace isc::data;
 using namespace isc::http;
 using namespace isc::http::test;
 using namespace isc::util;
-namespace ph = std::placeholders;
 
 /// @todo: put the common part of client and server tests in its own file(s).
 
index 58a7c91e1cdb28de0f9b2a617001a3f9dd518354..91a582f95816c73ed7f9af029551ad942075aa83 100644 (file)
@@ -21,7 +21,7 @@
 
 #include <exceptions/exceptions.h>
 
-#include <util/filename.h>
+#include <util/filesystem.h>
 #include <util/strutil.h>
 
 #include <log/log_messages.h>
@@ -33,7 +33,7 @@
 
 using namespace std;
 using namespace isc::log;
-using namespace isc::util;
+using namespace isc::util::file;
 
 /// \file log/compiler/message.cc
 /// \brief Message Compiler
@@ -89,14 +89,13 @@ usage() {
 /// &lt;name&gt;_&lt;ext&gt;, where &lt;name&gt; is the name of the file, and &lt;ext&gt;
 /// is the extension less the leading period.  The sentinel will be upper-case.
 ///
-/// \param file Filename object representing the file.
+/// \param file path to the file.
 ///
 /// \return Sentinel name
 
 string
-sentinel(Filename& file) {
-
-    string name = file.name();
+sentinel(Path& file) {
+    string name = file.stem();
     string ext = file.extension();
     string sentinel_text = name + "_" + ext.substr(1);
     isc::util::str::uppercase(sentinel_text);
@@ -227,10 +226,10 @@ writeHeaderFile(const string& file,
                 const vector<string>& ns_components,
                 MessageDictionary& dictionary,
                 const char* output_directory) {
-    Filename message_file(file);
-    Filename header_file(Filename(message_file.name()).useAsDefault(".h"));
+    Path message_file(file);
+    Path header_file(Path(file).replaceExtension(".h"));
     if (output_directory != NULL) {
-        header_file.setDirectory(output_directory);
+        header_file.replaceParentPath(output_directory);
     }
 
     // Text to use as the sentinels.
@@ -240,11 +239,11 @@ writeHeaderFile(const string& file,
     errno = 0;
 
     // Open the output file for writing
-    ofstream hfile(header_file.fullName().c_str());
+    ofstream hfile(header_file.str());
 
     if (hfile.fail()) {
         isc_throw_4(MessageException, "Failed to open output file",
-                    LOG_OPEN_OUTPUT_FAIL, header_file.fullName(),
+                    LOG_OPEN_OUTPUT_FAIL, header_file.str(),
                     strerror(errno), 0);
     }
 
@@ -252,7 +251,7 @@ writeHeaderFile(const string& file,
     // after the last write.
 
     hfile <<
-        "// File created from " << message_file.fullName() << "\n" <<
+        "// File created from " << message_file.str() << "\n" <<
          "\n" <<
          "#ifndef " << sentinel_text << "\n" <<
          "#define "  << sentinel_text << "\n" <<
@@ -277,7 +276,7 @@ writeHeaderFile(const string& file,
     // Report errors (if any) and exit
     if (hfile.fail()) {
         isc_throw_4(MessageException, "Error writing to output file",
-                    LOG_WRITE_ERROR, header_file.fullName(), strerror(errno),
+                    LOG_WRITE_ERROR, header_file.str(), strerror(errno),
                     0);
     }
 
@@ -330,21 +329,21 @@ writeProgramFile(const string& file,
                  const vector<string>& ns_components,
                  MessageDictionary& dictionary,
                  const char* output_directory) {
-    Filename message_file(file);
-    Filename program_file(Filename(message_file.name()).useAsDefault(".cc"));
+    Path message_file(file);
+    Path program_file(Path(file).replaceExtension(".cc"));
     if (output_directory) {
-        program_file.setDirectory(output_directory);
+        program_file.replaceParentPath(output_directory);
     }
 
     // zero out the errno to be safe
     errno = 0;
 
     // Open the output file for writing
-    ofstream ccfile(program_file.fullName().c_str());
+    ofstream ccfile(program_file.str());
 
     if (ccfile.fail()) {
         isc_throw_4(MessageException, "Error opening output file",
-                    LOG_OPEN_OUTPUT_FAIL, program_file.fullName(),
+                    LOG_OPEN_OUTPUT_FAIL, program_file.str(),
                     strerror(errno), 0);
     }
 
@@ -352,7 +351,7 @@ writeProgramFile(const string& file,
     // the last write.
 
     ccfile <<
-        "// File created from " << message_file.fullName() << "\n" <<
+        "// File created from " << message_file.str() << "\n" <<
          "\n" <<
          "#include <cstddef>\n" <<
          "#include <log/message_types.h>\n" <<
@@ -398,7 +397,7 @@ writeProgramFile(const string& file,
     // Report errors (if any) and exit
     if (ccfile.fail()) {
         isc_throw_4(MessageException, "Error writing to output file",
-                    LOG_WRITE_ERROR, program_file.fullName(), strerror(errno),
+                    LOG_WRITE_ERROR, program_file.str(), strerror(errno),
                     0);
     }
 
index 8602f804766c2af9dbf92687a78113caf6285b7c..f25029cdc1422530960ce308f8eee17037d8776f 100644 (file)
@@ -12,7 +12,7 @@
 #include <database/db_log.h>
 #include <exceptions/exceptions.h>
 #include <mysql/mysql_connection.h>
-#include <util/file_utilities.h>
+#include <util/filesystem.h>
 
 #include <boost/lexical_cast.hpp>
 
index c677fc7bbf9274b54a876ba7dbe4151addffe336..68b7a4cba429a205d3c6ee81a72be6fe4b7b4844 100644 (file)
@@ -12,7 +12,7 @@
 #include <database/db_exceptions.h>
 #include <database/db_log.h>
 #include <pgsql/pgsql_connection.h>
-#include <util/file_utilities.h>
+#include <util/filesystem.h>
 
 #include <exception>
 #include <unordered_map>
index 3ca8579d895da011a1f78143dee325be66b92c0d..9b795f8074e0b60b7886c27368966ca3de93802a 100644 (file)
@@ -14,7 +14,7 @@
 #include <log/logger_support.h>
 #include <process/config_base.h>
 #include <process/redact_config.h>
-#include <util/filename.h>
+#include <util/filesystem.h>
 
 #include <functional>
 #include <sstream>
@@ -23,7 +23,7 @@
 #include <errno.h>
 
 using namespace isc::data;
-namespace ph = std::placeholders;
+using namespace isc::util::file;
 
 /// @brief provides default implementation for basic daemon operations
 ///
@@ -116,10 +116,10 @@ Daemon::checkConfigFile() const {
         isc_throw(isc::BadValue, "config file name is not set");
     }
 
-    // Create Filename instance from the config_file_ pathname, and
+    // Create Path instance from the config_file_ pathname, and
     // check the file name component.
-    isc::util::Filename file(config_file_);
-    if (file.name().empty()) {
+    Path file(config_file_);
+    if (file.stem().empty()) {
         isc_throw(isc::BadValue, "config file:" << config_file_
                   << " is missing file name");
     }
@@ -176,10 +176,10 @@ Daemon::makePIDFileName() const {
                   "Daemon::makePIDFileName config file name is not set");
     }
 
-    // Create Filename instance from the config_file_ pathname, so we can
+    // Create Path instance from the config_file_ pathname, so we can
     // extract the fname component.
-    isc::util::Filename file(config_file_);
-    if (file.name().empty()) {
+    Path file(config_file_);
+    if (file.stem().empty()) {
         isc_throw(isc::BadValue, "Daemon::makePIDFileName config file:"
                   << config_file_ << " is missing file name");
     }
@@ -192,7 +192,7 @@ Daemon::makePIDFileName() const {
     // Make the pathname for the PID file from the runtime directory,
     // configuration name and process name.
     std::ostringstream stream;
-    stream  << pid_file_dir_ << "/" << file.name()
+    stream  << pid_file_dir_ << "/" << file.stem()
             << "." << proc_name_ << ".pid";
 
     return(stream.str());
index cc5799c9a1ca196cc6317f03edf4f7bdf9992d75..bab1def0aa3978ad8f1b0e466c1b8f76ac5bba40 100644 (file)
@@ -15,8 +15,7 @@ libkea_util_la_SOURCES += chrono_time_utils.h chrono_time_utils.cc
 libkea_util_la_SOURCES += csv_file.h csv_file.cc
 libkea_util_la_SOURCES += dhcp_space.h dhcp_space.cc
 libkea_util_la_SOURCES += doubles.h
-libkea_util_la_SOURCES += file_utilities.h file_utilities.cc
-libkea_util_la_SOURCES += filename.h filename.cc
+libkea_util_la_SOURCES += filesystem.h filesystem.cc
 libkea_util_la_SOURCES += hash.h
 libkea_util_la_SOURCES += labeled_value.h labeled_value.cc
 libkea_util_la_SOURCES += memory_segment.h
@@ -60,8 +59,7 @@ libkea_util_include_HEADERS = \
        chrono_time_utils.h \
        dhcp_space.h \
        doubles.h \
-       file_utilities.h \
-       filename.h \
+       filesystem.h \
        hash.h \
        io_utilities.h \
        labeled_value.h \
diff --git a/src/lib/util/file_utilities.cc b/src/lib/util/file_utilities.cc
deleted file mode 100644 (file)
index 7ae5169..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2021-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 <util/filename.h>
-#include <cerrno>
-#include <cstring>
-#include <fcntl.h>
-#include <sys/stat.h>
-
-using namespace std;
-
-namespace isc {
-namespace util {
-namespace file {
-
-string
-getContent(const string& file_name) {
-    // Open the file.
-    int fd = ::open(file_name.c_str(), O_RDONLY);
-    if (fd < 0) {
-        isc_throw(BadValue, "can't open file '" << file_name << "': "
-                  << std::strerror(errno));
-    }
-    try {
-        struct stat stats;
-        if (fstat(fd, &stats) < 0) {
-            isc_throw(BadValue, "can't stat file '" << file_name << "': "
-                      << std::strerror(errno));
-        }
-        if ((stats.st_mode & S_IFMT) != S_IFREG) {
-            isc_throw(BadValue, "'" << file_name
-                      << "' must be a regular file");
-        }
-        string content(stats.st_size, ' ');
-        ssize_t got = ::read(fd, &content[0], stats.st_size);
-        if (got < 0) {
-            isc_throw(BadValue, "can't read file '" << file_name << "': "
-                      << std::strerror(errno));
-        }
-        if (got != stats.st_size) {
-            isc_throw(BadValue, "can't read whole file '" << file_name
-                      << "' (got " << got << " of " << stats.st_size << ")");
-        }
-        static_cast<void>(close(fd));
-        return (content);
-    } catch (const std::exception&) {
-        static_cast<void>(close(fd));
-        throw;
-    }
-}
-
-bool
-isDir(const string& name) {
-    struct stat stats;
-    if (::stat(name.c_str(), &stats) < 0) {
-        return (false);
-    }
-    return ((stats.st_mode & S_IFMT) == S_IFDIR);
-}
-
-bool
-isFile(const string& name) {
-    struct stat stats;
-    if (::stat(name.c_str(), &stats) < 0) {
-        return (false);
-    }
-    return ((stats.st_mode & S_IFMT) == S_IFREG);
-}
-
-} // namespace file
-} // namespace log
-} // namespace isc
diff --git a/src/lib/util/file_utilities.h b/src/lib/util/file_utilities.h
deleted file mode 100644 (file)
index 06daa13..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2021-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/.
-
-#ifndef FILE_UTILITIES_H
-#define FILE_UTILITIES_H
-
-#include <string>
-
-namespace isc {
-namespace util {
-namespace file {
-
-/// @brief Get the content of a regular file.
-///
-/// @param file_name The file name.
-/// @return The content of the file_name file.
-/// @throw BadValue when the file can't be opened or is not a regular one.
-std::string getContent(const std::string& file_name);
-
-/// @brief Check if there is a directory at the given path.
-///
-/// @param name the path being checked.
-/// @return True if the name points to a directory, false otherwise including
-/// if the pointed location does not exist.
-bool isDir(const std::string& name);
-
-/// @brief Check if there is a file at the given path.
-///
-/// @param name the path being checked.
-/// @return True if the name points to a file, false otherwise including
-/// if the pointed location does not exist.
-bool isFile(const std::string& name);
-
-} // namespace file
-} // namespace util
-} // namespace isc
-
-#endif // FILE_UTILITIES_H
diff --git a/src/lib/util/filename.cc b/src/lib/util/filename.cc
deleted file mode 100644 (file)
index 8cd9cd3..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright (C) 2011-2021 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 <iostream>
-#include <algorithm>
-#include <string>
-
-#include <ctype.h>
-
-#include <util/filename.h>
-
-using namespace std;
-
-
-namespace isc {
-namespace util {
-
-// Split string into components.  Any backslashes are assumed to have
-// been replaced by forward slashes.
-
-void
-Filename::split(const string& full_name, string& directory, string& name,
-                string& extension) const {
-    directory = name = extension = "";
-    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.
-            directory = full_name.substr(0, last_slash + 1);
-            if (last_slash == full_name.size()) {
-
-                // The entire string was a directory, so exit not 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.
-            name = 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) {
-            name = full_name.substr(last_slash + 1, last_dot - last_slash - 1);
-        }
-    }
-
-}
-
-// Expand the stored filename with the default.
-
-string
-Filename::expandWithDefault(const string& defname) const {
-
-    string def_directory("");
-    string def_name("");
-    string def_extension("");
-
-    // Normalize the input string.
-    string copy_defname = isc::util::str::trim(defname);
-#ifdef WIN32
-    isc::util::str::normalizeSlash(copy_defname);
-#endif
-
-    // Split into the components
-    split(copy_defname, def_directory, def_name, def_extension);
-
-    // Now construct the result.
-    string retstring =
-        (directory_.empty() ? def_directory : directory_) +
-        (name_.empty() ? def_name : name_) +
-        (extension_.empty() ? def_extension : extension_);
-    return (retstring);
-}
-
-// Use the stored name as default for a given name
-
-string
-Filename::useAsDefault(const string& name) const {
-
-    string name_directory("");
-    string name_name("");
-    string name_extension("");
-
-    // Normalize the input string.
-    string copy_name = isc::util::str::trim(name);
-#ifdef WIN32
-    isc::util::str::normalizeSlash(copy_name);
-#endif
-
-    // Split into the components
-    split(copy_name, name_directory, name_name, name_extension);
-
-    // Now construct the result.
-    string retstring =
-        (name_directory.empty() ? directory_ : name_directory) +
-        (name_name.empty() ? name_ : name_name) +
-        (name_extension.empty() ? extension_ : name_extension);
-    return (retstring);
-}
-
-void
-Filename::setDirectory(const std::string& new_directory) {
-    std::string directory(new_directory);
-
-    if (directory.length() > 0) {
-        // append '/' if necessary
-        size_t sep = directory.rfind('/');
-        if (sep == std::string::npos || sep < directory.size() - 1) {
-            directory += "/";
-        }
-    }
-    // and regenerate the full name
-    std::string full_name = directory + name_ + extension_;
-
-    directory_.swap(directory);
-    full_name_.swap(full_name);
-}
-
-
-} // namespace log
-} // namespace isc
diff --git a/src/lib/util/filename.h b/src/lib/util/filename.h
deleted file mode 100644 (file)
index ae5ccd2..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright (C) 2011-2021 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 FILENAME_H
-#define FILENAME_H
-
-#include <string>
-
-#include <util/strutil.h>
-
-namespace isc {
-namespace util {
-
-/// \brief Class to Manipulate Filenames
-///
-/// This is a utility class to manipulate filenames.  It repeats some of the
-/// features found in the Boost filename class, but is self-contained so avoids
-/// the need to link in the Boost library.
-///
-/// A Unix-style filename comprises three parts:
-///
-/// Directory - everything up to and including the last "/".  If there is no
-/// "/" in the string, there is no directory component.  Note that the
-/// requirement of a trailing slash eliminates the ambiguity of whether a
-/// component is a directory or not, e.g. in /alpha/beta", "beta" could be the
-/// name of a directory or is could be a file.  The interpretation here is that
-/// "beta" is the name of a file (although that file could be a directory).
-///
-/// Note: Under Windows, the drive letter is considered to be part of the
-/// directory specification.  Unless this class becomes more widely-used on
-/// Windows, there is no point in adding redundant code.
-///
-/// Name - everything from the character after the last "/" up to but not
-/// including the last ".".
-///
-/// Extension - everything from the right-most "." (after the right-most "/") to
-/// the end of the string.  If there is no "." after the last "/", there is
-/// no file extension.
-///
-/// (Note that on Windows, this function will replace all "\" characters
-/// with "/" characters on input strings.)
-///
-/// This class provides functions for extracting the components and for
-/// substituting components.
-
-
-class Filename {
-public:
-
-    /// \brief Constructor
-    Filename(const std::string& name) :
-        full_name_(""), directory_(""), name_(""), extension_("") {
-        setName(name);
-    }
-
-    /// \brief Sets Stored Filename
-    ///
-    /// \param name New name to replaced currently stored name
-    void setName(const std::string& name) {
-        full_name_ = isc::util::str::trim(name);
-#ifdef WIN32
-        isc::util::str::normalizeSlash(full_name_);
-#endif
-        split(full_name_, directory_, name_, extension_);
-    }
-
-    /// \return Stored Filename
-    std::string fullName() const {
-        return (full_name_);
-    }
-
-    /// \return Directory of Given File Name
-    std::string directory() const {
-        return (directory_);
-    }
-
-    /// \brief Set directory for the file
-    ///
-    /// \param new_directory The directory to set. If this is an empty
-    ///        string, the directory this filename object currently
-    ///        has will be removed.
-    void setDirectory(const std::string& new_directory);
-
-    /// \return Name of Given File Name
-    std::string name() const {
-        return (name_);
-    }
-
-    /// \return Extension of Given File Name
-    std::string extension() const {
-        return (extension_);
-    }
-
-    /// \return Name + extension of Given File Name
-    std::string nameAndExtension() const {
-        return (name_ + extension_);
-    }
-
-    /// \brief Expand Name with Default
-    ///
-    /// A default file specified is supplied and used to fill in any missing
-    /// fields.  For example, if the name stored is "/a/b" and the supplied
-    /// name is "c.d", the result is "/a/b.d": the only field missing from the
-    /// stored name is the extension, which is supplied by the default.
-    /// Another example would be to store "a.b" and to supply a default of
-    /// "/c/d/" - the result is "/c/d/a.b".  (Note that if the supplied default
-    /// was "/c/d", the result would be "/c/a.b", even if "/c/d" were actually
-    /// a directory.)
-    ///
-    /// \param defname Default name
-    ///
-    /// \return Name expanded with defname.
-    std::string expandWithDefault(const std::string& defname) const;
-
-    /// \brief Use as Default and Substitute into String
-    ///
-    /// Does essentially the inverse of expand(); that filled in the stored
-    /// name with a default and returned the result.  This treats the stored
-    /// name as the default and uses it to fill in a given name.  In essence,
-    /// the code:
-    /// \code
-    ///       Filename f("/a/b");
-    ///       result = f.expandWithdefault("c.d");
-    /// \endcode
-    /// gives as a result "/a/b.d".  This is the same as:
-    /// \code
-    ///       Filename f("c.d");
-    ///       result = f.useAsDefault("/a/b");
-    /// \endcode
-    ///
-    /// \param name Name to expand
-    ///
-    /// \return Name expanded with stored name
-    std::string useAsDefault(const std::string& name) const;
-
-private:
-    /// \brief Split Name into Components
-    ///
-    /// Splits the file name into the directory, name and extension parts.
-    /// The name is assumed to have had back slashes replaced by forward
-    /// slashes (if appropriate).
-    ///
-    /// \param full_name Name to split
-    /// \param directory Returned directory part
-    /// \param name Returned name part
-    /// \param extension Returned extension part
-    void split(const std::string& full_name, std::string& directory,
-               std::string& name, std::string& extension) const;
-
-    // Members
-
-    std::string full_name_;     ///< Given name
-    std::string directory_;     ///< Directory part
-    std::string name_;          ///< Name part
-    std::string extension_;     ///< Extension part
-};
-
-} // namespace util
-} // namespace isc
-
-#endif // FILENAME_H
diff --git a/src/lib/util/filesystem.cc b/src/lib/util/filesystem.cc
new file mode 100644 (file)
index 0000000..c23ee5a
--- /dev/null
@@ -0,0 +1,169 @@
+// Copyright (C) 2021-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 <util/filesystem.h>
+#include <util/str.h>
+
+#include <algorithm>
+#include <cctype>
+#include <cerrno>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <string>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+using namespace isc::util::str;
+using namespace std;
+
+namespace isc {
+namespace util {
+namespace file {
+
+string
+getContent(string const& file_name) {
+    if (!exists(file_name)) {
+        isc_throw(BadValue, "Expected a file at path '" << file_name << "'");
+    }
+    if (!isFile(file_name)) {
+        isc_throw(BadValue, "Expected '" << file_name << "' to be a regular file");
+    }
+    ifstream file(file_name, ios::in);
+    if (!file.is_open()) {
+        isc_throw(BadValue, "Cannot open '" << file_name);
+    }
+    string content;
+    file >> content;
+    return content;
+}
+
+bool
+exists(string const& path) {
+    struct stat statbuf;
+    return (::stat(path.c_str(), &statbuf) == 0);
+}
+
+bool
+isDir(string const& path) {
+    struct stat statbuf;
+    if (::stat(path.c_str(), &statbuf) < 0) {
+        return (false);
+    }
+    return ((statbuf.st_mode & S_IFMT) == S_IFDIR);
+}
+
+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);
+}
+
+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 + 1);
+            if (last_slash == full_name.size()) {
+                // The entire string was a directory, so exit not 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_ + 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.at(trimmed_replacement.size() - 1) == '/') {
+        parent_path_ = trimmed_replacement;
+    } else {
+        parent_path_ = trimmed_replacement + '/';
+    }
+    return *this;
+}
+
+}  // namespace file
+}  // namespace util
+}  // namespace isc
diff --git a/src/lib/util/filesystem.h b/src/lib/util/filesystem.h
new file mode 100644 (file)
index 0000000..edf16c4
--- /dev/null
@@ -0,0 +1,132 @@
+// Copyright (C) 2021-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/.
+
+#ifndef KEA_UTIL_FILESYSTEM_H
+#define KEA_UTIL_FILESYSTEM_H
+
+#include <string>
+
+namespace isc {
+namespace util {
+namespace file {
+
+/// \brief Get the content of a regular file.
+///
+/// \param file_name The file name.
+///
+/// \return The content of the file.
+/// \throw BadValue when the file can't be opened or is not a regular one.
+std::string
+getContent(const std::string& file_name);
+
+/// \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 directory at the given path.
+///
+/// \param path The path being checked.
+///
+/// \return True if the path points to a directory, false otherwise including
+/// if the pointed location does not exist.
+bool
+isDir(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 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 the file 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 extension of the file.
+    ///
+    /// Counterpart for std::filesystem::path::extension.
+    ///
+    /// \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_;
+};
+
+}  // namespace file
+}  // namespace util
+}  // namespace isc
+
+#endif  // KEA_UTIL_FILESYSTEM_H
diff --git a/src/lib/util/tests/file_utilities_unittest.cc b/src/lib/util/tests/file_utilities_unittest.cc
deleted file mode 100644 (file)
index b998cfe..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-// 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 <util/file_utilities.h>
-#include <gtest/gtest.h>
-#include <fstream>
-
-using namespace isc;
-using namespace isc::util::file;
-using namespace std;
-
-namespace {
-
-/// @brief Test fixture class for testing operations on files.
-class FileUtilTest : public ::testing::Test {
-public:
-
-    /// @brief Destructor.
-    ///
-    /// Deletes the test file if any.
-    virtual ~FileUtilTest();
-};
-
-FileUtilTest::~FileUtilTest() {
-    string test_file_name(TEST_DATA_BUILDDIR "/fu.test");
-    static_cast<void>(remove(test_file_name.c_str()));
-}
-
-/// @brief Check an error is returned by getContent on not existent file.
-TEST_F(FileUtilTest, notExists) {
-    string file_name("/this/does/not/exists");
-    try {
-        string c = getContent(file_name);
-        FAIL() << "this test must throw before this line";
-    } catch (const BadValue& ex) {
-        string expected = "can't open file '" + file_name;
-        expected += "': No such file or directory";
-        EXPECT_EQ(string(ex.what()), expected);
-    } catch (const std::exception& ex) {
-        FAIL() << "unexpected exception: " << ex.what();
-    }
-}
-
-/// @note No easy can't stat.
-
-/// @brief Check an error is returned by getContent on not regular file.
-TEST_F(FileUtilTest, notRegular) {
-    string file_name("/");
-    try {
-        string c = getContent(file_name);
-        FAIL() << "this test must throw before this line";
-    } catch (const BadValue& ex) {
-        string expected = "'" + file_name + "' must be a regular file";
-        EXPECT_EQ(string(ex.what()), expected);
-    } catch (const std::exception& ex) {
-        FAIL() << "unexpected exception: " << ex.what();
-    }
-}
-
-/// @brief Check getContent works.
-TEST_F(FileUtilTest, basic) {
-    string file_name(TEST_DATA_BUILDDIR "/fu.test");
-    ofstream fs(file_name.c_str(), ofstream::out | ofstream::trunc);
-    ASSERT_TRUE(fs.is_open());
-    fs << "abdc";
-    fs.close();
-    string content;
-    EXPECT_NO_THROW(content = getContent(file_name));
-    EXPECT_EQ("abdc", content);
-}
-
-/// @brief Check isDir works.
-TEST_F(FileUtilTest, isDir) {
-    EXPECT_TRUE(isDir("/dev"));
-    EXPECT_FALSE(isDir("/dev/null"));
-    EXPECT_FALSE(isDir("/this/does/not/exist"));
-    EXPECT_FALSE(isDir("/etc/hosts"));
-}
-
-/// @brief Check isFile.
-TEST_F(FileUtilTest, isFile) {
-    EXPECT_TRUE(isFile(ABS_SRCDIR "/file_utilities_unittest.cc"));
-    EXPECT_FALSE(isFile(TEST_DATA_BUILDDIR));
-}
-
-}  // namespace
diff --git a/src/lib/util/tests/filename_unittest.cc b/src/lib/util/tests/filename_unittest.cc
deleted file mode 100644 (file)
index 39ff1f9..0000000
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (C) 2011-2015 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 <string>
-
-#include <gtest/gtest.h>
-
-#include <util/filename.h>
-
-using namespace isc;
-using namespace isc::util;
-using namespace std;
-
-class FilenameTest : public ::testing::Test {
-protected:
-    FilenameTest()
-    {
-    }
-};
-
-
-// Check that the name can be changed
-
-TEST_F(FilenameTest, SetName) {
-    Filename fname("/a/b/c.d");
-    EXPECT_EQ("/a/b/c.d", fname.fullName());
-
-    fname.setName("test.txt");
-    EXPECT_EQ("test.txt", fname.fullName());
-}
-
-
-// Check that the components are split correctly.  This is a check of the
-// private member split() method.
-
-TEST_F(FilenameTest, Components) {
-
-    // Complete name
-    Filename fname("/alpha/beta/gamma.delta");
-    EXPECT_EQ("/alpha/beta/", fname.directory());
-    EXPECT_EQ("gamma", fname.name());
-    EXPECT_EQ(".delta", fname.extension());
-    EXPECT_EQ("gamma.delta", fname.nameAndExtension());
-
-    // Directory only
-    fname.setName("/gamma/delta/");
-    EXPECT_EQ("/gamma/delta/", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("", fname.nameAndExtension());
-
-    // Filename only
-    fname.setName("epsilon");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("epsilon", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("epsilon", fname.nameAndExtension());
-
-    // Extension only
-    fname.setName(".zeta");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ(".zeta", fname.extension());
-    EXPECT_EQ(".zeta", fname.nameAndExtension());
-
-    // Missing directory
-    fname.setName("eta.theta");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("eta", fname.name());
-    EXPECT_EQ(".theta", fname.extension());
-    EXPECT_EQ("eta.theta", fname.nameAndExtension());
-
-    // Missing filename
-    fname.setName("/iota/.kappa");
-    EXPECT_EQ("/iota/", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ(".kappa", fname.extension());
-    EXPECT_EQ(".kappa", fname.nameAndExtension());
-
-    // Missing extension
-    fname.setName("lambda/mu/nu");
-    EXPECT_EQ("lambda/mu/", fname.directory());
-    EXPECT_EQ("nu", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("nu", fname.nameAndExtension());
-
-    // Check that the decomposition can occur in the presence of leading and
-    // trailing spaces
-    fname.setName("  lambda/mu/nu\t  ");
-    EXPECT_EQ("lambda/mu/", fname.directory());
-    EXPECT_EQ("nu", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("nu", fname.nameAndExtension());
-
-    // Empty string
-    fname.setName("");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("", fname.nameAndExtension());
-
-    // ... and just spaces
-    fname.setName("  ");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("", fname.nameAndExtension());
-
-    // Check corner cases - where separators are present, but strings are
-    // absent.
-    fname.setName("/");
-    EXPECT_EQ("/", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ("", fname.extension());
-    EXPECT_EQ("", fname.nameAndExtension());
-
-    fname.setName(".");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ(".", fname.extension());
-    EXPECT_EQ(".", fname.nameAndExtension());
-
-    fname.setName("/.");
-    EXPECT_EQ("/", fname.directory());
-    EXPECT_EQ("", fname.name());
-    EXPECT_EQ(".", fname.extension());
-    EXPECT_EQ(".", fname.nameAndExtension());
-
-    // Note that the space is a valid filename here; only leading and trailing
-    // spaces should be trimmed.
-    fname.setName("/ .");
-    EXPECT_EQ("/", fname.directory());
-    EXPECT_EQ(" ", fname.name());
-    EXPECT_EQ(".", fname.extension());
-    EXPECT_EQ(" .", fname.nameAndExtension());
-
-    fname.setName(" / . ");
-    EXPECT_EQ("/", fname.directory());
-    EXPECT_EQ(" ", fname.name());
-    EXPECT_EQ(".", fname.extension());
-    EXPECT_EQ(" .", fname.nameAndExtension());
-}
-
-// Check that the expansion with a default works.
-
-TEST_F(FilenameTest, ExpandWithDefault) {
-    Filename fname("a.b");
-
-    // These tests also check that the trimming of the default component is
-    // done properly.
-    EXPECT_EQ("/c/d/a.b", fname.expandWithDefault(" /c/d/  "));
-    EXPECT_EQ("/c/d/a.b", fname.expandWithDefault("/c/d/e.f"));
-    EXPECT_EQ("a.b", fname.expandWithDefault("e.f"));
-
-    fname.setName("/a/b/c");
-    EXPECT_EQ("/a/b/c.d", fname.expandWithDefault(".d"));
-    EXPECT_EQ("/a/b/c.d", fname.expandWithDefault("x.d"));
-    EXPECT_EQ("/a/b/c.d", fname.expandWithDefault("/s/t/u.d"));
-    EXPECT_EQ("/a/b/c", fname.expandWithDefault("/s/t/u"));
-
-    fname.setName(".h");
-    EXPECT_EQ("/a/b/c.h", fname.expandWithDefault("/a/b/c.msg"));
-}
-
-// Check that we can use this as a default in expanding a filename
-
-TEST_F(FilenameTest, UseAsDefault) {
-
-    Filename fname("a.b");
-
-    // These tests also check that the trimming of the default component is
-    // done properly.
-    EXPECT_EQ("/c/d/a.b", fname.useAsDefault(" /c/d/  "));
-    EXPECT_EQ("/c/d/e.f", fname.useAsDefault("/c/d/e.f"));
-    EXPECT_EQ("e.f", fname.useAsDefault("e.f"));
-
-    fname.setName("/a/b/c");
-    EXPECT_EQ("/a/b/c.d", fname.useAsDefault(".d"));
-    EXPECT_EQ("/a/b/x.d", fname.useAsDefault("x.d"));
-    EXPECT_EQ("/s/t/u.d", fname.useAsDefault("/s/t/u.d"));
-    EXPECT_EQ("/s/t/u", fname.useAsDefault("/s/t/u"));
-    EXPECT_EQ("/a/b/c", fname.useAsDefault(""));
-}
-
-TEST_F(FilenameTest, setDirectory) {
-    Filename fname("a.b");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("a.b", fname.fullName());
-    EXPECT_EQ("a.b", fname.expandWithDefault(""));
-
-    fname.setDirectory("/just/some/dir/");
-    EXPECT_EQ("/just/some/dir/", fname.directory());
-    EXPECT_EQ("/just/some/dir/a.b", fname.fullName());
-    EXPECT_EQ("/just/some/dir/a.b", fname.expandWithDefault(""));
-
-    fname.setDirectory("/just/some/dir");
-    EXPECT_EQ("/just/some/dir/", fname.directory());
-    EXPECT_EQ("/just/some/dir/a.b", fname.fullName());
-    EXPECT_EQ("/just/some/dir/a.b", fname.expandWithDefault(""));
-
-    fname.setDirectory("/");
-    EXPECT_EQ("/", fname.directory());
-    EXPECT_EQ("/a.b", fname.fullName());
-    EXPECT_EQ("/a.b", fname.expandWithDefault(""));
-
-    fname.setDirectory("");
-    EXPECT_EQ("", fname.directory());
-    EXPECT_EQ("a.b", fname.fullName());
-    EXPECT_EQ("a.b", fname.expandWithDefault(""));
-
-    fname = Filename("/first/a.b");
-    EXPECT_EQ("/first/", fname.directory());
-    EXPECT_EQ("/first/a.b", fname.fullName());
-    EXPECT_EQ("/first/a.b", fname.expandWithDefault(""));
-
-    fname.setDirectory("/just/some/dir");
-    EXPECT_EQ("/just/some/dir/", fname.directory());
-    EXPECT_EQ("/just/some/dir/a.b", fname.fullName());
-    EXPECT_EQ("/just/some/dir/a.b", fname.expandWithDefault(""));
-}
diff --git a/src/lib/util/tests/filesystem_unittests.cc b/src/lib/util/tests/filesystem_unittests.cc
new file mode 100644 (file)
index 0000000..548070a
--- /dev/null
@@ -0,0 +1,125 @@
+// 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 Test fixture class for testing operations on files.
+struct FileUtilTest : ::testing::Test {
+    /// @brief Destructor.
+    ///
+    /// Deletes the test file if any.
+    virtual ~FileUtilTest() {
+        string test_file_name(TEST_DATA_BUILDDIR "/fu.test");
+        static_cast<void>(remove(test_file_name.c_str()));
+    }
+};
+
+/// @brief Check that an error is returned by getContent on non-existent file.
+TEST_F(FileUtilTest, notExist) {
+    EXPECT_THROW_MSG(getContent("/does/not/exist"), BadValue,
+                     "Expected a file at path '/does/not/exist'");
+}
+
+/// @brief Check that an error is returned by getContent on not regular file.
+TEST_F(FileUtilTest, notRegular) {
+    EXPECT_THROW_MSG(getContent("/"), BadValue, "Expected '/' to be a regular file");
+}
+
+/// @brief Check getContent.
+TEST_F(FileUtilTest, getContent) {
+    string file_name(TEST_DATA_BUILDDIR "/fu.test");
+    ofstream fs(file_name.c_str(), ofstream::out | ofstream::trunc);
+    ASSERT_TRUE(fs.is_open());
+    fs << "abdc";
+    fs.close();
+    string content;
+    EXPECT_NO_THROW_LOG(content = getContent(file_name));
+    EXPECT_EQ("abdc", content);
+}
+
+/// @brief Check isDir.
+TEST_F(FileUtilTest, isDir) {
+    EXPECT_TRUE(isDir("/dev"));
+    EXPECT_FALSE(isDir("/dev/null"));
+    EXPECT_FALSE(isDir("/this/does/not/exist"));
+    EXPECT_FALSE(isDir("/etc/hosts"));
+}
+
+/// @brief Check isFile.
+TEST_F(FileUtilTest, isFile) {
+    EXPECT_TRUE(isFile(ABS_SRCDIR "/filesystem_unittests.cc"));
+    EXPECT_FALSE(isFile(TEST_DATA_BUILDDIR));
+}
+
+/// @brief Check that the components are split correctly.
+TEST(PathTest, components) {
+    // Complete name
+    Path fname("/alpha/beta/gamma.delta");
+    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", 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