# HG changeset patch # User David Demelier # Date 1475665560 -7200 # Node ID 24bb45724dc09c2d2320baf25a349ba9e4c2f515 # Parent f986f94c1510e791a8de2c8c1c9bfcbd49c6bb1b Irccd: split lib into libcommon, #564 diff -r f986f94c1510 -r 24bb45724dc0 CMakeLists.txt --- a/CMakeLists.txt Tue Nov 08 13:29:08 2016 +0100 +++ b/CMakeLists.txt Wed Oct 05 13:06:00 2016 +0200 @@ -78,23 +78,24 @@ add_subdirectory(extern/libircclient) add_subdirectory(extern/json) add_subdirectory(doc) -add_subdirectory(lib) -add_subdirectory(irccd) -add_subdirectory(irccdctl) -add_subdirectory(contrib) +add_subdirectory(libcommon) +#add_subdirectory(lib) +#add_subdirectory(irccd) +#add_subdirectory(irccdctl) +#add_subdirectory(contrib) if (WITH_JS) - add_subdirectory(plugins) +#add_subdirectory(plugins) endif () # Platform specific. if (WIN32) - add_subdirectory(win32) + # add_subdirectory(win32) endif () # Tests. include(CTest) -add_subdirectory(tests) +# add_subdirectory(tests) message("Compiling with the following flags:") message(" General flags: ${CMAKE_CXX_FLAGS}") diff -r f986f94c1510 -r 24bb45724dc0 libcommon/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/CMakeLists.txt Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,40 @@ +project(libcommon) + +set( + HEADERS + ${libcommon_SOURCE_DIR}/irccd/elapsed-timer.hpp + ${libcommon_SOURCE_DIR}/irccd/fs.hpp + ${libcommon_SOURCE_DIR}/irccd/ini.hpp + ${libcommon_SOURCE_DIR}/irccd/logger.hpp + ${libcommon_SOURCE_DIR}/irccd/net.hpp + ${libcommon_SOURCE_DIR}/irccd/options.hpp + ${libcommon_SOURCE_DIR}/irccd/path.hpp + ${libcommon_SOURCE_DIR}/irccd/signals.hpp + ${libcommon_SOURCE_DIR}/irccd/system.hpp + ${libcommon_SOURCE_DIR}/irccd/util.hpp + ${libcommon_SOURCE_DIR}/irccd/xdg.hpp +) + +set( + SOURCES + ${libcommon_SOURCE_DIR}/irccd/elapsed-timer.cpp + ${libcommon_SOURCE_DIR}/irccd/fs.cpp + ${libcommon_SOURCE_DIR}/irccd/ini.cpp + ${libcommon_SOURCE_DIR}/irccd/logger.cpp + ${libcommon_SOURCE_DIR}/irccd/options.cpp + ${libcommon_SOURCE_DIR}/irccd/path.cpp + ${libcommon_SOURCE_DIR}/irccd/system.cpp + ${libcommon_SOURCE_DIR}/irccd/util.cpp +) + +irccd_define_library( + TARGET libcommon + SOURCES + ${libcommon_SOURCE_DIR}/CMakeLists.txt + ${HEADERS} + ${SOURCES} + LIBRARIES extern-cppformat extern-json + PUBLIC_INCLUDES + $ + $ +) diff -r f986f94c1510 -r 24bb45724dc0 libcommon/irccd/elapsed-timer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/irccd/elapsed-timer.cpp Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,64 @@ +/* + * elapsed-timer.cpp -- measure elapsed time + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "elapsed-timer.hpp" + +using std::chrono::duration_cast; +using std::chrono::high_resolution_clock; +using std::chrono::milliseconds; + +namespace irccd { + +ElapsedTimer::ElapsedTimer() noexcept +{ + m_last = high_resolution_clock::now(); +} + +void ElapsedTimer::pause() noexcept +{ + /* + * When we put the timer on pause, do not forget to set the already + * elapsed time. + */ + (void)elapsed(); + m_paused = true; +} + +void ElapsedTimer::restart() noexcept +{ + m_paused = false; + m_last = high_resolution_clock::now(); +} + +void ElapsedTimer::reset() noexcept +{ + m_elapsed = 0; + m_last = high_resolution_clock::now(); +} + +unsigned ElapsedTimer::elapsed() noexcept +{ + if (!m_paused) { + m_elapsed += duration_cast(high_resolution_clock::now() - m_last).count(); + m_last = high_resolution_clock::now(); + } + + return m_elapsed; +} + +} // !irccd diff -r f986f94c1510 -r 24bb45724dc0 libcommon/irccd/elapsed-timer.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/irccd/elapsed-timer.hpp Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,87 @@ +/* + * elapsed-timer.hpp -- measure elapsed time + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef IRCCD_ELAPSED_TIMER_HPP +#define IRCCD_ELAPSED_TIMER_HPP + +/** + * \file elapsed-timer.hpp + * \brief Measure elapsed time + */ + +#include + +#include "sysconfig.hpp" + +namespace irccd { + +/** + * \class ElapsedTimer + * \brief Measure elapsed time + * + * This class provides an abstraction to measure elapsed time since the + * construction of the object. + * + * It uses std::chrono::high_resolution_clock for more precision and uses + * milliseconds only. + */ +class ElapsedTimer { +private: + using TimePoint = std::chrono::time_point; + + TimePoint m_last; + bool m_paused{false}; + unsigned m_elapsed{0}; + +public: + /** + * Construct the elapsed timer, start counting. + */ + IRCCD_EXPORT ElapsedTimer() noexcept; + + /** + * Virtual destructor defaulted. + */ + virtual ~ElapsedTimer() = default; + + /** + * Put the timer on pause, the already elapsed time is stored. + */ + IRCCD_EXPORT void pause() noexcept; + + /** + * Restart the timer, does not reset it. + */ + IRCCD_EXPORT void restart() noexcept; + + /** + * Reset the timer to 0. + */ + IRCCD_EXPORT void reset() noexcept; + + /** + * Get the number of elapsed milliseconds. + * + * \return the milliseconds + */ + IRCCD_EXPORT unsigned elapsed() noexcept; +}; + +} // !irccd + +#endif // !IRCCD_ELAPSED_TIMER_HPP diff -r f986f94c1510 -r 24bb45724dc0 libcommon/irccd/fs.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/irccd/fs.cpp Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,503 @@ +/* + * fs.cpp -- filesystem operations + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#if defined(_WIN32) +# if !defined(_CRT_SECURE_NO_WARNINGS) +# define _CRT_SECURE_NO_WARNINGS +# endif +# if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +# endif +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# include +# include +# include +#else +# include +# include +# include +#endif + +#include "fs.hpp" + +namespace irccd { + +namespace fs { + +namespace { + +/* + * error. + * ------------------------------------------------------------------ + * + * Function to retrieve system error in Windows API. + */ +#if defined(_WIN32) + +std::string error() +{ + LPSTR error = nullptr; + std::string errmsg = "Unknown error"; + + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&error, 0, nullptr); + + if (error) { + errmsg = std::string(error); + LocalFree(error); + } + + return errmsg; +} + +#endif + +/* + * hasAccess. + * ------------------------------------------------------------------ + * + * Check if we have access to the file specified, mode is the same used as + * std::fopen. + */ +bool hasAccess(const std::string &path, const std::string &mode) +{ + assert(mode.length() == 1); + assert(mode[0] == 'r' || mode[0] == 'w'); + + auto fp = std::fopen(path.c_str(), mode.c_str()); + + if (fp == nullptr) + return false; + + std::fclose(fp); + + return true; +} + +/* + * typeOf. + * ------------------------------------------------------------------ + * + * Get the type of the specified file. + * + * Use GetFileAttributesA on Windows and stat if available. + * + * Receives the object as predicate parameter and return true on success. + */ +#if defined(_WIN32) + +template +bool typeOf(const std::string &path, Predicate &&predicate) +{ + DWORD result = GetFileAttributesA(path.c_str()); + + if (result == INVALID_FILE_ATTRIBUTES) + return false; + + return predicate(result); +} + +#elif defined(FS_HAVE_STAT) + +template +bool typeOf(const std::string &path, Predicate &&predicate) noexcept +{ + struct stat st; + + if (::stat(path.c_str(), &st) < 0) + return false; + + return predicate(st); +} + +#else + +template +bool typeOf(const std::string &path, Predicate &&predicate) noexcept +{ + throw std::runtime_error(std::strerror(ENOSYS)); +} + +#endif + +} // !namespace + +/* + * clean. + * ------------------------------------------------------------------ + */ +std::string clean(std::string input) +{ + if (input.empty()) + return input; + + // First, remove any duplicates. + input.erase(std::unique(input.begin(), input.end(), [&] (char c1, char c2) { + return c1 == c2 && (c1 == '/' || c1 == '\\'); + }), input.end()); + + // Add a trailing / or \\. + char c = input[input.length() - 1]; + + if (c != '/' && c != '\\') + input += separator(); + + // Now converts all / to \\ for Windows and the opposite for Unix. +#if defined(_WIN32) + std::replace(input.begin(), input.end(), '/', '\\'); +#else + std::replace(input.begin(), input.end(), '\\', '/'); +#endif + + return input; +} + +/* + * baseName. + * ------------------------------------------------------------------ + */ +std::string baseName(std::string path) +{ + auto pos = path.find_last_of("\\/"); + + if (pos != std::string::npos) + path = path.substr(pos + 1); + + return path; +} + +/* + * dirName. + * ------------------------------------------------------------------ + */ +std::string dirName(std::string path) +{ + auto pos = path.find_last_of("\\/"); + + if (pos == std::string::npos) + path = "."; + else + path = path.substr(0, pos); + + return path; +} + +/* + * isAbsolute. + * ------------------------------------------------------------------ + */ +bool isAbsolute(const std::string &path) noexcept +{ +#if defined(_WIN32) + return !isRelative(path); +#else + return path.size() > 0 && path[0] == '/'; +#endif +} + +/* + * isRelative. + * ------------------------------------------------------------------ + */ +bool isRelative(const std::string &path) noexcept +{ +#if defined(_WIN32) + return PathIsRelativeA(path.c_str()) == 1; +#else + return !isAbsolute(path); +#endif +} + +/* + * isReadable. + * ------------------------------------------------------------------ + */ +bool isReadable(const std::string &path) noexcept +{ + return hasAccess(path, "r"); +} + +/* + * isWritable. + * ------------------------------------------------------------------ + */ +bool isWritable(const std::string &path) noexcept +{ + return hasAccess(path, "w"); +} + +/* + * isFile. + * ------------------------------------------------------------------ + */ +bool isFile(const std::string &path) +{ + return typeOf(path, [] (const auto &object) { +#if defined(_WIN32) + return (object & FILE_ATTRIBUTE_ARCHIVE) == FILE_ATTRIBUTE_ARCHIVE; +#elif defined(FS_HAVE_STAT) + return S_ISREG(object.st_mode); +#endif + }); +} + +/* + * isDirectory. + * ------------------------------------------------------------------ + */ +bool isDirectory(const std::string &path) +{ + return typeOf(path, [] (const auto &object) { +#if defined(_WIN32) + return (object & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; +#elif defined(FS_HAVE_STAT) + return S_ISDIR(object.st_mode); +#endif + }); +} + +/* + * isSymlink. + * ------------------------------------------------------------------ + */ +bool isSymlink(const std::string &path) +{ + return typeOf(path, [] (const auto &object) { +#if defined(_WIN32) + return (object & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT; +#elif defined(FS_HAVE_STAT) + return S_ISLNK(object.st_mode); +#endif + }); +} + +/* + * stat. + * ------------------------------------------------------------------ + */ +#if defined(FS_HAVE_STAT) + +struct stat stat(const std::string &path) +{ + struct stat st; + + if (::stat(path.c_str(), &st) < 0) + throw std::runtime_error(std::strerror(errno)); + + return st; +} + +#endif + +/* + * exists. + * ------------------------------------------------------------------ + */ +bool exists(const std::string &path) noexcept +{ +#if defined(FS_HAVE_STAT) + struct stat st; + + return ::stat(path.c_str(), &st) == 0; +#else + return hasAccess(path, "r"); +#endif +} + +/* + * readdir. + * ------------------------------------------------------------------ + */ +std::vector readdir(const std::string &path, int flags) +{ + std::vector entries; + +#if defined(_WIN32) + std::ostringstream oss; + HANDLE handle; + WIN32_FIND_DATA fdata; + + oss << path << "\\*"; + handle = FindFirstFile(oss.str().c_str(), &fdata); + + if (handle == nullptr) + throw std::runtime_error(error()); + + do { + Entry entry; + + entry.name = fdata.cFileName; + + if (entry.name == "." && !(flags & Dot)) + continue; + if (entry.name == ".." && !(flags & DotDot)) + continue; + + switch (fdata.dwFileAttributes) { + case FILE_ATTRIBUTE_DIRECTORY: + entry.type = Entry::Dir; + break; + case FILE_ATTRIBUTE_NORMAL: + entry.type = Entry::File; + break; + case FILE_ATTRIBUTE_REPARSE_POINT: + entry.type = Entry::Link; + break; + default: + break; + } + + entries.push_back(std::move(entry)); + } while (FindNextFile(handle, &fdata) != 0); + + FindClose(handle); +#else + DIR *dp; + struct dirent *ent; + + if ((dp = opendir(path.c_str())) == nullptr) + throw std::runtime_error(std::strerror(errno)); + + while ((ent = readdir(dp)) != nullptr) { + Entry entry; + + entry.name = ent->d_name; + if (entry.name == "." && !(flags & Dot)) + continue; + if (entry.name == ".." && !(flags & DotDot)) + continue; + + switch (ent->d_type) { + case DT_DIR: + entry.type = Entry::Dir; + break; + case DT_REG: + entry.type = Entry::File; + break; + case DT_LNK: + entry.type = Entry::Link; + break; + default: + break; + } + + entries.push_back(std::move(entry)); + } + + closedir(dp); +#endif + + return entries; +} + +/* + * mkdir. + * ------------------------------------------------------------------ + */ +void mkdir(const std::string &path, int mode) +{ + std::string::size_type next = 0; + std::string part; + + for (;;) { + next = path.find_first_of("\\/", next); + part = path.substr(0, next); + + if (!part.empty()) { +#if defined(_WIN32) + (void)mode; + + if (::_mkdir(part.c_str()) < 0 && errno != EEXIST) + throw std::runtime_error(std::strerror(errno)); +#else + if (::mkdir(part.c_str(), mode) < 0 && errno != EEXIST) + throw std::runtime_error(std::strerror(errno)); +#endif + } + + if (next++ == std::string::npos) + break; + } +} + +/* + * rmdir. + * ------------------------------------------------------------------ + */ +void rmdir(const std::string &base) noexcept +{ + try { + for (const auto &entry : readdir(base)) { + std::string path = base + separator() + entry.name; + + if (entry.type == Entry::Dir) + rmdir(path); + else + ::remove(path.c_str()); + } + } catch (...) { + // Silently discard to remove as much as possible. + } + +#if defined(_WIN32) + ::RemoveDirectoryA(base.c_str()); +#else + ::remove(base.c_str()); +#endif +} + +/* + * cwd. + * ------------------------------------------------------------------ + */ +std::string cwd() +{ +#if defined(_WIN32) + char path[MAX_PATH]; + + if (!::GetCurrentDirectoryA(sizeof (path), path)) + throw std::runtime_error("failed to get current working directory"); + + return path; +#else + char path[PATH_MAX]; + + if (::getcwd(path, sizeof (path)) == nullptr) + throw std::runtime_error{std::strerror(errno)}; + + return path; +#endif +} + +} // !irccd + +} // !fs diff -r f986f94c1510 -r 24bb45724dc0 libcommon/irccd/fs.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/irccd/fs.hpp Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,359 @@ +/* + * fs.hpp -- filesystem operations + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef FS_HPP +#define FS_HPP + +/** + * \file fs.hpp + * \brief Filesystem operations made easy. + */ + +/** + * \cond FS_HIDDEN_SYMBOLS + */ + +#if !defined(FS_HAVE_STAT) +# if defined(_WIN32) +# define FS_HAVE_STAT +# elif defined(__linux__) +# define FS_HAVE_STAT +# elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) +# define FS_HAVE_STAT +# elif defined(__APPLE__) +# define FS_HAVE_STAT +# endif +#endif + +/** + * \endcond + */ + +#if defined(FS_HAVE_STAT) +# include +#endif + +#include +#include +#include + +#include "sysconfig.hpp" + +namespace irccd { + +/** + * \brief Filesystem namespace. + */ +namespace fs { + +/** + * \enum Flags + * \brief Flags for readdir. + */ +enum Flags { + Dot = (1 << 0), //!< if set, also lists "." + DotDot = (1 << 1) //!< if set, also lists ".." +}; + +/** + * \class Entry + * \brief Entry in the directory list. + */ +class Entry { +public: + /** + * \brief Describe the type of an entry + */ + enum Type : char { + Unknown, //!< File type is unknown, + File, //!< File is regular type, + Dir, //!< File is directory, + Link //!< File is link + }; + + std::string name; //!< name of entry (base name) + Type type{Unknown}; //!< type of file +}; + +/** + * Check if two entries are identical. + * + * \param e1 the first entry + * \param e2 the second entry + * \return true if they are identical + */ +inline bool operator==(const Entry &e1, const Entry &e2) noexcept +{ + return e1.name == e2.name && e1.type == e2.type; +} + +/** + * Check if two entries are different. + * + * \param e1 the first entry + * \param e2 the second entry + * \return true if they are different + */ +inline bool operator!=(const Entry &e1, const Entry &e2) noexcept +{ + return !(e1 == e2); +} + +/** + * Get the separator for that system. + * + * \return \ on Windows and / otherwise + */ +inline char separator() noexcept +{ +#if defined(_WIN32) + return '\\'; +#else + return '/'; +#endif +} + +/** + * Clean a path by removing any extra / or \ and add a trailing one. + * + * \param path the path + * \return the updated path + */ +IRCCD_EXPORT std::string clean(std::string path); + +/** + * Get the base name from a path. + * + * Example, baseName("/etc/foo.conf") // foo.conf + * + * \param path the path + * \return the base name + */ +IRCCD_EXPORT std::string baseName(std::string path); + +/** + * Get the parent directory from a path. + * + * Example, dirName("/etc/foo.conf") // /etc + * + * \param path the path + * \return the parent directory + */ +IRCCD_EXPORT std::string dirName(std::string path); + +#if defined(FS_HAVE_STAT) + +/** + * Get stat information. + * + * \param path the path + * \return the stat information + * \throw std::runtime_error on failure + */ +IRCCD_EXPORT struct stat stat(const std::string &path); + +#endif // !HAVE_STAT + +/** + * Check if a file exists. + * + * If HAVE_ACCESS is defined, the function access is used, otherwise stat is + * used. + * + * \param path the path to check + * \return true if the path exists + */ +IRCCD_EXPORT bool exists(const std::string &path) noexcept; + +/** + * Check if the path is absolute. + * + * \param path the path + * \return true if the path is absolute + */ +IRCCD_EXPORT bool isAbsolute(const std::string &path) noexcept; + +/** + * Check if the path is relative. + * + * \param path the path + * \return true if the path is absolute + */ +IRCCD_EXPORT bool isRelative(const std::string &path) noexcept; + +/** + * Check if the file is readable. + * + * \param path the path + * \return true if has read access + */ +IRCCD_EXPORT bool isReadable(const std::string &path) noexcept; + +/** + * Check if the file is writable. + * + * \param path the path + * \return true if has write access + */ +IRCCD_EXPORT bool isWritable(const std::string &path) noexcept; + +/** + * Check if the file is a regular file. + * + * \param path the path + * \return true if it is a file and false if not or not readable + * \throw std::runtime_error if the operation is not supported + */ +IRCCD_EXPORT bool isFile(const std::string &path); + +/** + * Check if the file is a directory. + * + * \param path the path + * \return true if it is a directory and false if not or not readable + * \throw std::runtime_error if the operation is not supported + */ +IRCCD_EXPORT bool isDirectory(const std::string &path); + +/** + * Check if the file is a symbolic link. + * + * \param path the path + * \return true if it is a symbolic link and false if not or not readable + * \throw std::runtime_error if the operation is not supported + */ +IRCCD_EXPORT bool isSymlink(const std::string &path); + +/** + * Read a directory and return a list of entries (not recursive). + * + * \param path the directory path + * \param flags the optional flags (see Flags) + * \return the list of entries + * \throw std::runtime_error on failure + */ +IRCCD_EXPORT std::vector readdir(const std::string &path, int flags = 0); + +/** + * Create a directory recursively. + * + * \param path the path + * \param mode the optional mode (not always supported) + * \throw std::runtime_error on failure + * \post all intermediate directories are created + */ +IRCCD_EXPORT void mkdir(const std::string &path, int mode = 0700); + +/** + * Remove a directory recursively. + * + * If errors happens, they are silently discarded to remove as much as possible. + * + * \param path the path + */ +IRCCD_EXPORT void rmdir(const std::string &path) noexcept; + +/** + * Search an item recursively. + * + * The predicate must have the following signature: + * void f(const std::string &base, const Entry &entry) + * + * Where: + * - base is the current parent directory in the tree + * - entry is the current entry + * + * \param base the base directory + * \param predicate the predicate + * \return the full path name to the file or empty string if never found + * \throw std::runtime_error on read errors + */ +template +std::string findIf(const std::string &base, Predicate &&predicate) +{ + /* + * Do not go deeply to the tree before testing all files in the current + * directory for performances reasons, we iterate this directory to search + * for the entry name and iterate again over all sub directories if not + * found. + */ + std::string path; + std::vector entries = readdir(base); + + for (const auto &entry : entries) { + if (predicate(base, entry)) { + path = base + separator() + entry.name; + break; + } + } + + if (!path.empty()) + return path; + + for (const auto &entry : entries) { + if (entry.type != Entry::Dir) + continue; + + path = findIf(base + separator() + entry.name, std::forward(predicate)); + + if (!path.empty()) + break; + } + + return path; +} + +/** + * Find a file by name recursively. + * + * \param base the base directory + * \param name the file name + * \return the full path name to the file or empty string if never found + * \throw std::runtime_error on read errors + */ +inline std::string find(const std::string &base, const std::string &name) +{ + return findIf(base, [&] (const auto &, const auto &entry) { return entry.name == name; }); +} + +/** + * Overload by regular expression. + * + * \param base the base directory + * \param regex the regular expression + * \return the full path name to the file or empty string if never found + * \throw std::runtime_error on read errors + */ +inline std::string find(const std::string &base, const std::regex ®ex) +{ + return findIf(base, [&] (const auto &, const auto &entry) { return std::regex_match(entry.name, regex); }); +} + +/** + * Get the current working directory. + * + * \return the current working directory + * \throw std::runtime_error on failure + */ +IRCCD_EXPORT std::string cwd(); + +} // !fs + +} // !irccd + +#endif // !FS_HPP diff -r f986f94c1510 -r 24bb45724dc0 libcommon/irccd/ini.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/irccd/ini.cpp Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,420 @@ +/* + * ini.cpp -- extended .ini file parser + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +// for PathIsRelative. +#if defined(_WIN32) +# if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +# endif + +# include +#endif + +#include "ini.hpp" + +namespace irccd { + +namespace { + +using namespace ini; + +using StreamIterator = std::istreambuf_iterator; +using TokenIterator = std::vector::const_iterator; + +inline bool isAbsolute(const std::string &path) noexcept +{ +#if defined(_WIN32) + return !PathIsRelative(path.c_str()); +#else + return path.size() > 0 && path[0] == '/'; +#endif +} + +inline bool isQuote(char c) noexcept +{ + return c == '\'' || c == '"'; +} + +inline bool isSpace(char c) noexcept +{ + // Custom version because std::isspace includes \n as space. + return c == ' ' || c == '\t'; +} + +inline bool isList(char c) noexcept +{ + return c == '(' || c == ')' || c == ','; +} + +inline bool isReserved(char c) noexcept +{ + return isList(c) || isQuote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '='; +} + +void analyseLine(int &line, int &column, StreamIterator &it) noexcept +{ + assert(*it == '\n'); + + ++ line; + ++ it; + column = 0; +} + +void analyseComment(int &column, StreamIterator &it, StreamIterator end) noexcept +{ + assert(*it == '#'); + + while (it != end && *it != '\n') { + ++ column; + ++ it; + } +} + +void analyseSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept +{ + assert(isSpace(*it)); + + while (it != end && isSpace(*it)) { + ++ column; + ++ it; + } +} + +void analyseList(Tokens &list, int line, int &column, StreamIterator &it) noexcept +{ + assert(isList(*it)); + + switch (*it++) { + case '(': + list.emplace_back(Token::ListBegin, line, column++); + break; + case ')': + list.emplace_back(Token::ListEnd, line, column++); + break; + case ',': + list.emplace_back(Token::Comma, line, column++); + break; + default: + break; + } +} + +void analyseSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + assert(*it == '['); + + std::string value; + int save = column; + + // Read section name. + ++ it; + while (it != end && *it != ']') { + if (*it == '\n') + throw Error(line, column, "section not terminated, missing ']'"); + if (isReserved(*it)) + throw Error(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'"); + + ++ column; + value += *it++; + } + + if (it == end) + throw Error(line, column, "section name expected after '[', got "); + if (value.empty()) + throw Error(line, column, "empty section name"); + + // Remove ']'. + ++ it; + + list.emplace_back(Token::Section, line, save, std::move(value)); +} + +void analyseAssign(Tokens &list, int &line, int &column, StreamIterator &it) +{ + assert(*it == '='); + + list.push_back({ Token::Assign, line, column++ }); + ++ it; +} + +void analyseQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + std::string value; + int save = column; + char quote = *it++; + + while (it != end && *it != quote) { + // TODO: escape sequence + ++ column; + value += *it++; + } + + if (it == end) + throw Error(line, column, "undisclosed '" + std::string(1, quote) + "', got "); + + // Remove quote. + ++ it; + + list.push_back({ Token::QuotedWord, line, save, std::move(value) }); +} + +void analyseWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + assert(!isReserved(*it)); + + std::string value; + int save = column; + + while (it != end && !std::isspace(*it) && !isReserved(*it)) { + ++ column; + value += *it++; + } + + list.push_back({ Token::Word, line, save, std::move(value) }); +} + +void analyseInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + assert(*it == '@'); + + std::string include; + int save = column; + + // Read include. + ++ it; + while (it != end && !isSpace(*it)) { + ++ column; + include += *it++; + } + + if (include != "include") + throw Error(line, column, "expected include after '@' token"); + + list.push_back({ Token::Include, line, save }); +} + +void parseOptionValueSimple(Option &option, TokenIterator &it) +{ + assert(it->type() == Token::Word || it->type() == Token::QuotedWord); + + option.push_back((it++)->value()); +} + +void parseOptionValueList(Option &option, TokenIterator &it, TokenIterator end) +{ + assert(it->type() == Token::ListBegin); + + TokenIterator save = it++; + + while (it != end && it->type() != Token::ListEnd) { + switch (it->type()) { + case Token::Comma: + // Previous must be a word. + if (it[-1].type() != Token::Word && it[-1].type() != Token::QuotedWord) + throw Error(it->line(), it->column(), "unexpected comma after '" + it[-1].value() + "'"); + + ++ it; + break; + case Token::Word: + case Token::QuotedWord: + option.push_back((it++)->value()); + break; + default: + throw Error(it->line(), it->column(), "unexpected '" + it[-1].value() + "' in list construct"); + break; + } + } + + if (it == end) + throw Error(save->line(), save->column(), "unterminated list construct"); + + // Remove ). + ++ it; +} + +void parseOption(Section &sc, TokenIterator &it, TokenIterator end) +{ + Option option(it->value()); + + TokenIterator save = it; + + // No '=' or something else? + if (++it == end) + throw Error(save->line(), save->column(), "expected '=' assignment, got "); + if (it->type() != Token::Assign) + throw Error(it->line(), it->column(), "expected '=' assignment, got " + it->value()); + + // Empty options are allowed so just test for words. + if (++it != end) { + if (it->type() == Token::Word || it->type() == Token::QuotedWord) + parseOptionValueSimple(option, it); + else if (it->type() == Token::ListBegin) + parseOptionValueList(option, it, end); + } + + sc.push_back(std::move(option)); +} + +void parseInclude(Document &doc, const std::string &path, TokenIterator &it, TokenIterator end) +{ + TokenIterator save = it; + + if (++it == end) + throw Error(save->line(), save->column(), "expected file name after '@include' statement, got "); + if (it->type() != Token::Word && it->type() != Token::QuotedWord) + throw Error(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value()); + + std::string value = (it++)->value(); + std::string file; + + if (!isAbsolute(value)) +#if defined(_WIN32) + file = path + "\\" + value; +#else + file = path + "/" + value; +#endif + else + file = value; + + for (const auto &sc : readFile(file)) + doc.push_back(sc); +} + +void parseSection(Document &doc, TokenIterator &it, TokenIterator end) +{ + Section sc(it->value()); + + // Skip [section]. + ++ it; + + // Read until next section. + while (it != end && it->type() != Token::Section) { + if (it->type() != Token::Word) + throw Error(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition"); + + parseOption(sc, it, end); + } + + doc.push_back(std::move(sc)); +} + +} // !namespace + +namespace ini { + +Tokens analyse(std::istreambuf_iterator it, std::istreambuf_iterator end) +{ + Tokens list; + int line = 1; + int column = 0; + + while (it != end) { + if (*it == '\n') + analyseLine(line, column, it); + else if (*it == '#') + analyseComment(column, it, end); + else if (*it == '[') + analyseSection(list, line, column, it, end); + else if (*it == '=') + analyseAssign(list, line, column, it); + else if (isSpace(*it)) + analyseSpaces(column, it, end); + else if (*it == '@') + analyseInclude(list, line, column, it, end); + else if (isQuote(*it)) + analyseQuotedWord(list, line, column, it, end); + else if (isList(*it)) + analyseList(list, line, column, it); + else + analyseWord(list, line, column, it, end); + } + + return list; +} + +Tokens analyse(std::istream &stream) +{ + return analyse(std::istreambuf_iterator(stream), {}); +} + +Document parse(const Tokens &tokens, const std::string &path) +{ + Document doc; + TokenIterator it = tokens.cbegin(); + TokenIterator end = tokens.cend(); + + while (it != end) { + switch (it->type()) { + case Token::Include: + parseInclude(doc, path, it, end); + break; + case Token::Section: + parseSection(doc, it, end); + break; + default: + throw Error(it->line(), it->column(), "unexpected '" + it->value() + "' on root document"); + } + } + + return doc; +} + +Document readFile(const std::string &filename) +{ + // Get parent path. + auto parent = filename; + auto pos = parent.find_last_of("/\\"); + + if (pos != std::string::npos) + parent.erase(pos); + else + parent = "."; + + std::ifstream input(filename); + + if (!input) + throw Error(0, 0, std::strerror(errno)); + + return parse(analyse(input), parent); +} + +Document readString(const std::string &buffer) +{ + std::istringstream iss(buffer); + + return parse(analyse(iss)); +} + +void dump(const Tokens &tokens) +{ + for (const Token &token: tokens) + // TODO: add better description + std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl; +} + +} // !ini + +} // !irccd diff -r f986f94c1510 -r 24bb45724dc0 libcommon/irccd/ini.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libcommon/irccd/ini.hpp Wed Oct 05 13:06:00 2016 +0200 @@ -0,0 +1,616 @@ +/* + * ini.hpp -- extended .ini file parser + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef INI_HPP +#define INI_HPP + +/** + * \file ini.hpp + * \brief Extended .ini file parser. + * \author David Demelier + */ + +/** + * \page Ini Ini + * \brief Extended .ini file parser. + * + * - \subpage ini-syntax + */ + +/** + * \page ini-syntax Syntax + * \brief File syntax. + * + * The syntax is similar to most of `.ini` implementations as: + * + * - a section is delimited by `[name]` can be redefined multiple times, + * - an option **must** always be defined in a section, + * - empty options must be surrounded by quotes, + * - lists can not includes trailing commas, + * - include statement must always be at the beginning of files (in no sections), + * - comments starts with # until the end of line, + * - options with spaces **must** use quotes. + * + * # Basic file + * + * ````ini + * # This is a comment. + * [section] + * option1 = value1 + * option2 = "value 2 with spaces" # comment is also allowed here + * ```` + * + * # Redefinition + * + * Sections can be redefined multiple times and are kept the order they are seen. + * + * ````ini + * [section] + * value = "1" + * + * [section] + * value = "2" + * ```` + * + * The ini::Document object will contains two ini::Section. + * + * # Lists + * + * Lists are defined using `()` and commas, like values, they may have quotes. + * + * ````ini + * [section] + * names = ( "x1", "x2" ) + * + * # This is also allowed + * biglist = ( + * "abc", + * "def" + * ) + * ```` + * + * # Include statement + * + * You can split a file into several pieces, if the include statement contains a relative path, the path will be relative + * to the current file being parsed. + * + * You **must** use the include statement before any section. + * + * If the file contains spaces, use quotes. + * + * ````ini + * # main.conf + * @include "foo.conf" + * + * # foo.conf + * [section] + * option1 = value1 + * ```` + */ + +#include +#include +#include +#include +#include +#include + +#include "sysconfig.hpp" + +namespace irccd { + +/** + * Namespace for ini related classes. + */ +namespace ini { + +class Document; + +/** + * \class Error + * \brief Error in a file. + */ +class Error : public std::exception { +private: + int m_line; //!< line number + int m_column; //!< line column + std::string m_message; //!< error message + +public: + /** + * Constructor. + * + * \param line the line + * \param column the column + * \param msg the message + */ + inline Error(int line, int column, std::string msg) noexcept + : m_line(line) + , m_column(column) + , m_message(std::move(msg)) + { + } + + /** + * Get the line number. + * + * \return the line + */ + inline int line() const noexcept + { + return m_line; + } + + /** + * Get the column number. + * + * \return the column + */ + inline int column() const noexcept + { + return m_column; + } + + /** + * Return the raw error message (no line and column shown). + * + * \return the error message + */ + const char *what() const noexcept override + { + return m_message.c_str(); + } +}; + +/** + * \class Token + * \brief Describe a token read in the .ini source. + * + * This class can be used when you want to parse a .ini file yourself. + * + * \see analyze + */ +class Token { +public: + /** + * \brief Token type. + */ + enum Type { + Include, //!< include statement + Section, //!< [section] + Word, //!< word without quotes + QuotedWord, //!< word with quotes + Assign, //!< = assignment + ListBegin, //!< begin of list ( + ListEnd, //!< end of list ) + Comma //!< list separation + }; + +private: + Type m_type; + int m_line; + int m_column; + std::string m_value; + +public: + /** + * Construct a token. + * + * \param type the type + * \param line the line + * \param column the column + * \param value the value + */ + Token(Type type, int line, int column, std::string value = "") noexcept + : m_type(type) + , m_line(line) + , m_column(column) + { + switch (type) { + case Include: + m_value = "@include"; + break; + case Section: + case Word: + case QuotedWord: + m_value = value; + break; + case Assign: + m_value = "="; + break; + case ListBegin: + m_value = "("; + break; + case ListEnd: + m_value = ")"; + break; + case Comma: + m_value = ","; + break; + default: + break; + } + } + + /** + * Get the type. + * + * \return the type + */ + inline Type type() const noexcept + { + return m_type; + } + + /** + * Get the line. + * + * \return the line + */ + inline int line() const noexcept + { + return m_line; + } + + /** + * Get the column. + * + * \return the column + */ + inline int column() const noexcept + { + return m_column; + } + + /** + * Get the value. For words, quoted words and section, the value is the content. Otherwise it's the + * characters parsed. + * + * \return the value + */ + inline const std::string &value() const noexcept + { + return m_value; + } +}; + +/** + * List of tokens in order they are analyzed. + */ +using Tokens = std::vector; + +/** + * \class Option + * \brief Option definition. + */ +class Option : public std::vector { +private: + std::string m_key; + +public: + /** + * Construct an empty option. + * + * \pre key must not be empty + * \param key the key + */ + inline Option(std::string key) noexcept + : std::vector() + , m_key(std::move(key)) + { + assert(!m_key.empty()); + } + + /** + * Construct a single option. + * + * \pre key must not be empty + * \param key the key + * \param value the value + */ + inline Option(std::string key, std::string value) noexcept + : m_key(std::move(key)) + { + assert(!m_key.empty()); + + push_back(std::move(value)); + } + + /** + * Construct a list option. + * + * \pre key must not be empty + * \param key the key + * \param values the values + */ + inline Option(std::string key, std::vector values) noexcept + : std::vector(std::move(values)) + , m_key(std::move(key)) + { + assert(!m_key.empty()); + } + + /** + * Get the option key. + * + * \return the key + */ + inline const std::string &key() const noexcept + { + return m_key; + } + + /** + * Get the option value. + * + * \return the value + */ + inline const std::string &value() const noexcept + { + static std::string dummy; + + return empty() ? dummy : (*this)[0]; + } +}; + +/** + * \class Section + * \brief Section that contains one or more options. + */ +class Section : public std::vector