From: Andrei Pavel Date: Mon, 4 Mar 2024 09:48:36 +0000 (+0200) Subject: [#3210] refactor file utilities X-Git-Tag: Kea-2.5.7~18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a6935b0d55dbbbaac5214058d94490f21b9f5f3a;p=thirdparty%2Fkea.git [#3210] refactor file utilities - 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. --- diff --git a/src/bin/keactrl/keactrl.in b/src/bin/keactrl/keactrl.in index 7ad71e3668..22a3f9af4a 100644 --- a/src/bin/keactrl/keactrl.in +++ b/src/bin/keactrl/keactrl.in @@ -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) diff --git a/src/hooks/dhcp/high_availability/ha_config_parser.cc b/src/hooks/dhcp/high_availability/ha_config_parser.cc index a1faae6ea8..18747ae964 100644 --- a/src/hooks/dhcp/high_availability/ha_config_parser.cc +++ b/src/hooks/dhcp/high_availability/ha_config_parser.cc @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/lib/asiolink/common_tls.cc b/src/lib/asiolink/common_tls.cc index 35ca637bdc..4363be4878 100644 --- a/src/lib/asiolink/common_tls.cc +++ b/src/lib/asiolink/common_tls.cc @@ -10,7 +10,7 @@ #include #include -#include +#include using namespace isc::cryptolink; using namespace isc::util; diff --git a/src/lib/http/basic_auth_config.cc b/src/lib/http/basic_auth_config.cc index 3d96b98a62..1e6a727094 100644 --- a/src/lib/http/basic_auth_config.cc +++ b/src/lib/http/basic_auth_config.cc @@ -8,7 +8,7 @@ #include #include -#include +#include #include using namespace isc; diff --git a/src/lib/http/tests/tls_server_unittests.cc b/src/lib/http/tests/tls_server_unittests.cc index 29e50c1de5..cca66ac8f1 100644 --- a/src/lib/http/tests/tls_server_unittests.cc +++ b/src/lib/http/tests/tls_server_unittests.cc @@ -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). diff --git a/src/lib/log/compiler/message.cc b/src/lib/log/compiler/message.cc index 58a7c91e1c..91a582f958 100644 --- a/src/lib/log/compiler/message.cc +++ b/src/lib/log/compiler/message.cc @@ -21,7 +21,7 @@ #include -#include +#include #include #include @@ -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() { /// <name>_<ext>, where <name> is the name of the file, and <ext> /// 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& 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& 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 \n" << "#include \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); } diff --git a/src/lib/mysql/mysql_connection.cc b/src/lib/mysql/mysql_connection.cc index 8602f80476..f25029cdc1 100644 --- a/src/lib/mysql/mysql_connection.cc +++ b/src/lib/mysql/mysql_connection.cc @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include diff --git a/src/lib/pgsql/pgsql_connection.cc b/src/lib/pgsql/pgsql_connection.cc index c677fc7bbf..68b7a4cba4 100644 --- a/src/lib/pgsql/pgsql_connection.cc +++ b/src/lib/pgsql/pgsql_connection.cc @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include diff --git a/src/lib/process/daemon.cc b/src/lib/process/daemon.cc index 3ca8579d89..9b795f8074 100644 --- a/src/lib/process/daemon.cc +++ b/src/lib/process/daemon.cc @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include @@ -23,7 +23,7 @@ #include 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()); diff --git a/src/lib/util/Makefile.am b/src/lib/util/Makefile.am index cc5799c9a1..bab1def0aa 100644 --- a/src/lib/util/Makefile.am +++ b/src/lib/util/Makefile.am @@ -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 index 7ae5169afe..0000000000 --- a/src/lib/util/file_utilities.cc +++ /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 - -#include -#include -#include -#include -#include -#include - -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(close(fd)); - return (content); - } catch (const std::exception&) { - static_cast(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 index 06daa13656..0000000000 --- a/src/lib/util/file_utilities.h +++ /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 - -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 index 8cd9cd3e65..0000000000 --- a/src/lib/util/filename.cc +++ /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 - -#include -#include -#include - -#include - -#include - -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 index ae5ccd2565..0000000000 --- a/src/lib/util/filename.h +++ /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 - -#include - -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 index 0000000000..c23ee5a335 --- /dev/null +++ b/src/lib/util/filesystem.cc @@ -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 + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 index 0000000000..edf16c4ff2 --- /dev/null +++ b/src/lib/util/filesystem.h @@ -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 + +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 index b998cfe350..0000000000 --- a/src/lib/util/tests/file_utilities_unittest.cc +++ /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 - -#include -#include -#include -#include - -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(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 index 39ff1f9570..0000000000 --- a/src/lib/util/tests/filename_unittest.cc +++ /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 - -#include - -#include - -#include - -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 index 0000000000..548070a35a --- /dev/null +++ b/src/lib/util/tests/filesystem_unittests.cc @@ -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 + +#include +#include +#include + +#include +#include + +#include + +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(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