diff --git a/src/celcompat/CMakeLists.txt b/src/celcompat/CMakeLists.txt new file mode 100644 index 00000000..c7815dd1 --- /dev/null +++ b/src/celcompat/CMakeLists.txt @@ -0,0 +1,6 @@ +set(CELCOMPAT_SOURCES + fs.cpp + fs.h +) + +add_library(celcompat OBJECT ${CELCOMPAT_SOURCES}) diff --git a/src/celcompat/filesystem.h b/src/celcompat/filesystem.h new file mode 100644 index 00000000..abc41e99 --- /dev/null +++ b/src/celcompat/filesystem.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#if __cplusplus >= 201703L +#include +namespace fs = std::filesystem; +#elif defined(HAVE_EXPERIMENTAL_FILESYSTEM) +#include +namespace fs = std::experimental::filesystem; +#else +#include "fs.h" +namespace fs = celestia::filesystem; +#endif diff --git a/src/celcompat/fs.cpp b/src/celcompat/fs.cpp new file mode 100644 index 00000000..0f853dba --- /dev/null +++ b/src/celcompat/fs.cpp @@ -0,0 +1,415 @@ +#include "fs.h" +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + + +namespace celestia +{ +namespace filesystem +{ + +// we should skip "." and ".." +template static bool is_special_dir(const CharT s[]) +{ + return s[0] == '.' && (s[1] == '\0' || (s[1] == '.' && s[2] == '\0')); +} + +path operator/(const path& lhs, const path& rhs) +{ + return path(lhs) /= rhs; +} + +std::ostream& operator<<(std::ostream& os, const path& p) +{ + os << '"' << p.string() << '"'; + return os; +} + +path u8path(const std::string& source) +{ +#ifdef _WIN32 + return UTF8ToWide(source); +#else + return source; // FIXME +#endif +} + +#ifdef _WIN32 +void path::fixup_separators() +{ + if (m_fmt == native_format) + return; + + string_type::size_type pos = 0; + for(;;) + { + pos = m_path.find(L'/', pos); + if (pos == path::string_type::npos) + break; + m_path[pos] = preferred_separator; + pos++; + } +} +#endif + +path path::filename() const +{ + auto pos = m_path.rfind(preferred_separator); + if (pos == string_type::npos) + pos = 0; + else + pos++; + + auto fn = m_path.substr(pos); + if (is_special_dir(fn.c_str())) + return path(); + + path p(fn); + return p; +} + +path path::stem() const +{ + auto fn = filename().native(); + auto pos = fn.rfind('.'); + + if (pos == 0 || pos == string_type::npos) + return fn; + + return fn.substr(0, pos); +} + +path path::extension() const +{ + auto fn = filename().native(); + auto pos = fn.rfind('.'); + + if (pos == 0 || pos == string_type::npos) + return path(); + + return fn.substr(pos); +} + +path path::parent_path() const +{ + auto pos = m_path.rfind(preferred_separator); + if (pos == 0) + return path(m_path.substr(0, 1)); + + if (pos == string_type::npos) + return path(); + + return path(m_path.substr(0, pos)); +} + + +bool path::is_absolute() const +{ +#ifdef _WIN32 + return m_path[0] == preferred_separator || m_path[1] == L':'; +#else + return m_path[0] == preferred_separator; +#endif +} + +struct directory_iterator::SearchImpl +{ + path m_path {}; +#ifdef _WIN32 + HANDLE m_handle { INVALID_HANDLE_VALUE }; +#else + DIR *m_dir { nullptr }; +#endif + explicit SearchImpl(const path& p) : + m_path(p) + { + }; + ~SearchImpl(); + + bool advance(directory_entry&); +}; + +directory_iterator::SearchImpl::~SearchImpl() +{ +#ifdef _WIN32 + if (m_handle != INVALID_HANDLE_VALUE) + { + FindClose(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } +#else + if (m_dir != nullptr) + { + closedir(m_dir); + m_dir = nullptr; + } +#endif +} + +bool directory_iterator::SearchImpl::advance(directory_entry& entry) +{ +#ifdef _WIN32 + WIN32_FIND_DATAW findData; + + if (m_handle == INVALID_HANDLE_VALUE) + { + m_handle = FindFirstFileW((m_path / L"*").c_str(), &findData); + if (m_handle == INVALID_HANDLE_VALUE) + return false; + } + else + { + if (!FindNextFileW(m_handle, &findData)) + return false; + } + + while (is_special_dir(findData.cFileName)) + { + if (!FindNextFileW(m_handle, &findData)) + return false; + } + + entry = directory_entry(std::move(m_path / findData.cFileName)); + return true; +#else + if (m_dir == nullptr) + { + m_dir = opendir(m_path.c_str()); + if (m_dir == nullptr) + return false; + } + + struct dirent* ent = nullptr; + do + { + ent = readdir(m_dir); + if (ent == nullptr) + return false; + } + while (is_special_dir(ent->d_name)); + + entry = directory_entry(std::move(m_path / ent->d_name)); + return true; +#endif +} + +directory_iterator::directory_iterator(const path& p) : + directory_iterator::directory_iterator(p, m_ec) +{ +} + +directory_iterator::directory_iterator(const path& p, std::error_code& ec) : + m_path(p), + m_ec(ec), + m_search(std::make_shared(p)) +{ + if (!m_search->advance(m_entry)) + reset(); +} + +void directory_iterator::reset() +{ + *this = directory_iterator(); +} + +bool directory_iterator::operator==(const directory_iterator& other) const noexcept +{ + return m_search == other.m_search && m_entry == other.m_entry; +} + +directory_iterator& directory_iterator::operator++() +{ + // *this != end(*this) ... + if (m_search != nullptr && !m_search->advance(m_entry)) + reset(); + return *this; +} + + +struct recursive_directory_iterator::DirStack +{ + std::vector m_dirIters; +}; + +recursive_directory_iterator::recursive_directory_iterator(const path& p) +{ + if (m_dirs == nullptr) + m_dirs = std::make_shared(); + + m_iter = directory_iterator(p); +} + +bool recursive_directory_iterator::operator==(const recursive_directory_iterator& other) const noexcept +{ + return m_depth == other.m_depth && + m_pending == other.m_pending && + m_dirs == other.m_dirs && + m_iter == other.m_iter; +} + + +recursive_directory_iterator& recursive_directory_iterator::operator++() +{ + // *this == end(*this) + if (m_dirs == nullptr) + return *this; + + auto& p = m_iter->path(); + if (m_pending && is_directory(p)) + { + m_dirs->m_dirIters.emplace_back(m_iter); + m_iter = directory_iterator(p); + } + else + { + ++m_iter; + } + + while (m_iter == end(m_iter)) + { + if (m_dirs->m_dirIters.empty()) + { + reset(); + return *this; + } + m_iter = std::move(m_dirs->m_dirIters.back()); + ++m_iter; + m_dirs->m_dirIters.pop_back(); + } + + return *this; +} + +void recursive_directory_iterator::pop() +{ + std::error_code ec; + pop(ec); +} + +void recursive_directory_iterator::pop(std::error_code& ec) +{ + m_dirs->m_dirIters.pop_back(); +} + +void recursive_directory_iterator::reset() +{ + *this = recursive_directory_iterator(); +} + + +uintmax_t file_size(const path& p, std::error_code& ec) noexcept +{ +#ifdef _WIN32 + WIN32_FILE_ATTRIBUTE_DATA attr; + LARGE_INTEGER fileSize = { 0 }; + if (GetFileAttributesExW(p.c_str(), GetFileExInfoStandard, &attr)) + { + fileSize.LowPart = attr.nFileSizeLow; + fileSize.HighPart = attr.nFileSizeHigh; + return static_cast(fileSize.QuadPart); + } + else + { + ec = std::error_code(errno, std::system_category()); + return static_cast(-1); + } +#else + struct stat stat_buf; + int rc = stat(p.c_str(), &stat_buf); + if (rc == -1) + { + ec = std::error_code(errno, std::system_category()); + return static_cast(-1); + } + + return static_cast(stat_buf.st_size); +#endif +} + +uintmax_t file_size(const path& p) +{ + std::error_code ec; + uintmax_t s = file_size(p, ec); + if (ec) + throw filesystem_error(ec, "celfs::file_size error"); + return s; +} + + +bool exists(const path& p, std::error_code& ec) noexcept +{ +#ifdef _WIN32 + DWORD attr = GetFileAttributesW(&p.native()[0]); + if (attr != INVALID_FILE_ATTRIBUTES) + return true; + + switch (GetLastError()) + { + // behave like boost::filesystem + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_NAME: + case ERROR_INVALID_DRIVE: + case ERROR_NOT_READY: + case ERROR_INVALID_PARAMETER: + case ERROR_BAD_PATHNAME: + case ERROR_BAD_NETPATH: + return false; + default: + break; + } +#else + struct stat buf; + if (stat(p.c_str(), &buf) == 0) + return true; + + if (errno == ENOENT) + return false; +#endif + ec = std::error_code(errno, std::system_category()); + return false; +} + +bool exists(const path& p) +{ + std::error_code ec; + bool r = exists(p, ec); + if (ec) + throw filesystem_error(ec, "celfs::exists error"); + return r; +} + + +bool is_directory(const path& p, std::error_code& ec) noexcept +{ +#ifdef _WIN32 + DWORD attr = GetFileAttributesW(&p.native()[0]); + if (attr == INVALID_FILE_ATTRIBUTES) + { + ec = std::error_code(errno, std::system_category()); + return false; + } + + return (attr & FILE_ATTRIBUTE_DIRECTORY) != 0; +#else + struct stat buf; + return (stat(p.c_str(), &buf) == 0) && S_ISDIR(buf.st_mode); +#endif +} + +bool is_directory(const path& p) +{ + std::error_code ec; + bool r = is_directory(p, ec); + if (ec) + throw filesystem_error(ec, "celfs::is_directory error"); + return r; +} + +} +} diff --git a/src/celcompat/fs.h b/src/celcompat/fs.h new file mode 100644 index 00000000..65473a8e --- /dev/null +++ b/src/celcompat/fs.h @@ -0,0 +1,412 @@ +#pragma once + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + + +namespace celestia +{ +namespace filesystem +{ +class filesystem_error : std::system_error +{ + public: + filesystem_error(std::error_code ec, const char* msg) : + std::system_error(ec, msg) + { + } +}; // filesystem_error + + +class path +{ + public: +#ifdef _WIN32 + using string_type = std::wstring; +#else + using string_type = std::string; +#endif + using value_type = string_type::value_type; + +#ifdef _WIN32 + static constexpr value_type preferred_separator = L'\\'; +#else + static constexpr value_type preferred_separator = '/'; +#endif + + enum format + { + native_format, + generic_format, + auto_format + }; + + path() noexcept + { + }; + path(const path&) = default; + path(path&& p) noexcept : + m_path(std::move(p.m_path)), + m_fmt(p.m_fmt) + { + }; + path(string_type&& p, format fmt = auto_format) : + m_path(std::move(p)), + m_fmt(fmt) + { +#ifdef _WIN32 + fixup_separators(); +#endif + }; + template path(const T& p, format fmt = auto_format ) : + m_path(encconv(p)), + m_fmt(fmt) + { +#ifdef _WIN32 + fixup_separators(); +#endif + }; + ~path() = default; + path& operator=(const path& p) = default; + path& operator=(path&&) = default; + + template path& append(const T& p) + { + if (empty()) + m_path = p; + else + m_path.append(1, preferred_separator).append(p); + return *this; + } + + template path& operator/=(const T& p) + { + return append(p); + } + + template path& concat(const T& p) + { + m_path += p; + return *this; + } + + template path& operator+=(const T& p) + { + return concat(p); + } + + bool operator==(const path& other) const noexcept + { + return m_path == other.m_path && m_fmt == other.m_fmt; + } + + bool operator!=(const path& other) const noexcept + { + return !(*this == other); + } + + bool operator<(const path& other) const noexcept + { + return m_path < other.m_path; + } + + bool operator>(const path& other) const noexcept + { + return m_path > other.m_path; + } + + bool operator<=(const path& other) const noexcept + { + return m_path <= other.m_path; + } + + bool operator>=(const path& other) const noexcept + { + return m_path >= other.m_path; + } + + std::string string() const noexcept + { +#ifdef _WIN32 + return WideToCurrentCP(m_path); +#else + return m_path; +#endif + } + + std::wstring wstring() const noexcept + { +#ifdef _WIN32 + return m_path; +#else + return L""; // FIXME +#endif + } + + std::string u8string() const noexcept + { +#ifdef _WIN32 + return WideToUTF8(m_path); +#else + return m_path; // FIXME +#endif + } + + const value_type* c_str() const noexcept + { + return m_path.c_str(); + } + + const string_type& native() const noexcept + { + return m_path; + } + + operator string_type() const noexcept + { + return m_path; + } + + bool empty() const noexcept + { + return m_path.empty(); + } + + path filename() const; + path stem() const; + path extension() const; + path parent_path() const; + + bool is_relative() const + { + return !is_absolute(); + } + bool is_absolute() const; + + private: + inline string_type encconv(const std::string& p) const + { +#ifdef _WIN32 + return CurrentCPToWide(p); +#else + return p; +#endif + } + inline string_type encconv(const std::wstring& p) const + { +#ifdef _WIN32 + return p; +#else + return ""; // FIXME +#endif + } +#ifdef _WIN32 + void fixup_separators(); +#endif + + string_type m_path; + format m_fmt { auto_format }; +}; // path + +path operator/(const path& lhs, const path& rhs); + +std::ostream& operator<<(std::ostream& os, const path& p); + +path u8path(const std::string& source); + + +class directory_iterator; +class recursive_directory_iterator; +class directory_entry +{ + public: + directory_entry() = default; + directory_entry(const directory_entry&) = default; + directory_entry(directory_entry&&) = default; + explicit directory_entry(const path& p) : + m_path(p) + { + }; + directory_entry& operator=(const directory_entry&) = default; + directory_entry& operator=(directory_entry&&) = default; + + const class path& path() const noexcept + { + return m_path; + } + operator const class path& () const noexcept + { + return m_path; + }; + + bool operator==(const directory_entry& other) const noexcept + { + return m_path == other.m_path; + } + bool operator!=(const directory_entry& other) const noexcept + { + return !(*this == other); + } + + private: + friend class directory_iterator; + friend class recursive_directory_iterator; + + class path m_path; +}; // directory_entry + + +class directory_iterator +{ + public: + using value_type = directory_entry; + using difference_type = std::ptrdiff_t; + using pointer = const directory_entry*; + using reference = const directory_entry&; + using iterator_category = std::input_iterator_tag; + + directory_iterator() = default; + explicit directory_iterator(const path& p); + directory_iterator(const path& p, std::error_code& ec); + + directory_iterator(const directory_iterator&) = default; + directory_iterator(directory_iterator&&) = default; + ~directory_iterator() = default; + + directory_iterator& operator=(const directory_iterator&) = default; + directory_iterator& operator=(directory_iterator&&) = default; + + directory_iterator& operator++(); + + const directory_entry& operator*() const noexcept + { + return m_entry; + } + + const directory_entry* operator->() const noexcept + { + return &m_entry; + } + + bool operator==(const directory_iterator& other) const noexcept; + bool operator!=(const directory_iterator& other) const noexcept + { + return !(*this == other); + } + + private: + struct SearchImpl; + void reset(); + + path m_path {}; + directory_entry m_entry {}; + std::error_code m_ec {}; + std::shared_ptr m_search { nullptr }; +}; // directory_iterator + +inline directory_iterator begin(directory_iterator iter) noexcept +{ + return iter; +} + +inline directory_iterator end(directory_iterator) noexcept +{ + return directory_iterator(); +} + + +class recursive_directory_iterator +{ + public: + using value_type = directory_entry; + using difference_type = std::ptrdiff_t; + using pointer = const directory_entry*; + using reference = const directory_entry&; + using iterator_category = std::input_iterator_tag; + + recursive_directory_iterator() = default; + recursive_directory_iterator(const recursive_directory_iterator&) = default; + recursive_directory_iterator(recursive_directory_iterator&&) noexcept = default; + explicit recursive_directory_iterator(const path& p); +// recursive_directory_iterator(const path&, directory_options); +// recursive_directory_iterator(const path&, directory_options, std::error_code&); + recursive_directory_iterator(const path& p, std::error_code& ec); + ~recursive_directory_iterator() = default; + + recursive_directory_iterator& operator=(const recursive_directory_iterator&) = default; + recursive_directory_iterator& operator=(recursive_directory_iterator&&) = default; + + bool recursion_pending() const + { + return m_pending; + }; + + void disable_recursion_pending() + { + m_pending = false; + }; + + void pop(); + void pop(std::error_code& ec); + + recursive_directory_iterator& operator++(); + + const directory_entry& operator*() const noexcept + { + return *m_iter; + } + + const directory_entry* operator->() const noexcept + { + return &*m_iter; + } + + bool operator==(const recursive_directory_iterator& other) const noexcept; + bool operator!=(const recursive_directory_iterator& other) const noexcept + { + return !(*this == other); + } + + private: + void reset(); + + std::error_code m_ec {}; + int m_depth { 0 }; + bool m_pending { true }; + + struct DirStack; + std::shared_ptr m_dirs { nullptr }; + directory_iterator m_iter {}; +}; // recursive_directory_iterator + +inline recursive_directory_iterator begin(recursive_directory_iterator iter) noexcept +{ + return iter; +} + +inline recursive_directory_iterator end(recursive_directory_iterator) noexcept +{ + return recursive_directory_iterator(); +} + + +uintmax_t file_size(const path& p, std::error_code& ec) noexcept; +uintmax_t file_size(const path& p); + +bool exists(const path& p); +bool exists(const path& p, std::error_code& ec) noexcept; + +bool is_directory(const path& p); +bool is_directory(const path& p, std::error_code& ec) noexcept; +}; +}; diff --git a/src/celcompat/testfs.cc b/src/celcompat/testfs.cc new file mode 100644 index 00000000..17fab4d4 --- /dev/null +++ b/src/celcompat/testfs.cc @@ -0,0 +1,30 @@ +#include +#include "fs.h" +namespace fs = celestia::filesystem; + +int main() +{ + std::cout << fs::path("/foo/bar.txt").extension() << '\n' + << fs::path("/foo/bar.").extension() << '\n' + << fs::path("/foo/bar").extension() << '\n' + << fs::path("/foo/bar.txt/bar.cc").extension() << '\n' + << fs::path("/foo/bar.txt/bar.").extension() << '\n' + << fs::path("/foo/bar.txt/bar").extension() << '\n' + << fs::path("/foo/.").extension() << '\n' + << fs::path("/foo/..").extension() << '\n' + << fs::path("/foo/.hidden").extension() << '\n' + << fs::path("/foo/..bar").extension() << '\n'; + + std::cout << "----------\n"; + std::cout << fs::path("/foo/bar.txt").stem() << '\n' + << fs::path("/foo/.bar").stem() << '\n'; + + for (fs::path p = "foo.bar.baz.tar"; !p.extension().empty(); p = p.stem()) + std::cout << p.extension() << '\n'; + + std::cout << "----------\n"; + std::cout << fs::path("/foo/bar.txt") << '\n'; + std::cout << fs::path("baz/foo/bar.txt") << '\n'; + std::cout << fs::path("c:\\foo\\bar.txt") << '\n'; + std::cout << fs::path(L"c:\\foo\\bar.txt") << '\n'; +}