diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 04de69b..250feb7 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -1,9 +1,6 @@ name: trx-cpp tests on: - push: - paths: - - "**" pull_request: paths: - "**" @@ -11,6 +8,12 @@ on: jobs: build-and-test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + boost: + - "ON" + - "OFF" steps: - name: Checkout uses: actions/checkout@v4 @@ -25,6 +28,9 @@ jobs: libeigen3-dev \ nlohmann-json3-dev \ libspdlog-dev + if [ "${{ matrix.boost }}" = "ON" ]; then + sudo apt-get install -y libboost-filesystem-dev libboost-system-dev + fi - name: Fetch mio run: | @@ -48,10 +54,11 @@ jobs: -DCMAKE_INSTALL_PREFIX=${GITHUB_WORKSPACE}/deps/googletest/install cmake --build deps/googletest/build --target install - - name: Configure trx-cpp + - name: Configure trx-cpp (Boost ${{ matrix.boost }}) run: | cmake -S . -B build \ -DTRX_BUILD_TESTS=ON \ + -DTRX_USE_BOOST_FILESYSTEM=${{ matrix.boost }} \ -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio/include \ -DGTest_DIR=${GITHUB_WORKSPACE}/deps/googletest/install/lib/cmake/GTest \ -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/libzip/install @@ -64,6 +71,12 @@ jobs: conan-create: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + boost: + - "True" + - "False" steps: - name: Checkout uses: actions/checkout@v4 @@ -78,9 +91,9 @@ jobs: python3 -m pip install --upgrade pip python3 -m pip install "conan>=2.0,<3.0" - - name: Conan create (with tests) + - name: Conan create (with tests, Boost ${{ matrix.boost }}) env: CONAN_HOME: ${{ runner.temp }}/.conan2 run: | conan profile detect --force - conan create . --build=missing -o with_tests=True -s build_type=Release \ No newline at end of file + conan create . --build=missing -o with_tests=True -o with_boost=${{ matrix.boost }} -s build_type=Release \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a6766b..575a700 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,12 +7,13 @@ project(trx VERSION 0.1.0) include(GNUInstallDirs) include(CMakePackageConfigHelpers) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 11) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Debug) endif() option(TRX_USE_CONAN "Should Conan package manager be used?" ON) +option(TRX_USE_BOOST_FILESYSTEM "Use Boost filesystem if available" ON) option(TRX_BUILD_TESTS "Build trx tests" ON) find_package(libzip REQUIRED) @@ -39,6 +40,9 @@ if (NOT TARGET Eigen3::Eigen AND EXISTS "${EIGEN3_INCLUDE_DIR}") endif() find_package(nlohmann_json CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) +if(TRX_USE_BOOST_FILESYSTEM) + find_package(Boost QUIET COMPONENTS filesystem system) +endif() find_package(mio CONFIG QUIET) if(TARGET mio::mio) set(TRX_HAVE_MIO_TARGET ON) @@ -76,6 +80,15 @@ TARGET_LINK_LIBRARIES(trx spdlog::spdlog $<$:mio::mio> ) +if(TRX_USE_BOOST_FILESYSTEM AND Boost_FOUND) + target_compile_definitions(trx PUBLIC TRX_USE_BOOST_FILESYSTEM) + if(TARGET Boost::filesystem) + target_link_libraries(trx PUBLIC Boost::filesystem Boost::system) + else() + target_include_directories(trx PUBLIC ${Boost_INCLUDE_DIRS}) + target_link_libraries(trx PUBLIC ${Boost_FILESYSTEM_LIBRARY} ${Boost_SYSTEM_LIBRARY}) + endif() +endif() target_include_directories(trx PUBLIC $ diff --git a/README.md b/README.md index 5809b04..732151a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,130 @@ # TRX-cpp -The C++ implementations to the memory-mapped tractography file format. +TRX-cpp is a C++11 library for reading, writing, and memory-mapping the TRX +tractography file format (zip archives or on-disk directories of memmaps). -## Installation -### Dependencies -- c++11 compiler / cmake +## Dependencies + +Required: +- C++11 compiler and CMake (>= 3.10) - libzip - nlohmann::json - Eigen3 - spdlog -- GTest (optional) +- mio (header-only memory-mapped IO) + +Optional: +- GTest (for building tests) +- Boost.Filesystem + Boost.System (optional filesystem shim) + +## Build and Install + +### Quick build (no tests) + +``` +cmake -S . -B build +cmake --build build +``` + +### Build with tests + +``` +cmake -S . -B build \ + -DTRX_BUILD_TESTS=ON \ + -DGTest_DIR=/path/to/GTestConfig.cmake +cmake --build build +ctest --test-dir build --output-on-failure +``` + +### mio include path + +If `mio` is not available via your system include paths, point CMake at it: + +``` +cmake -S . -B build -DMIO_INCLUDE_DIR=/path/to/mio/include +``` + +### Filesystem shim + +By default, the build will try to use Boost.Filesystem if it is available. You +can force this behavior: + +``` +# Prefer Boost if available (default) +cmake -S . -B build -DTRX_USE_BOOST_FILESYSTEM=ON + +# Force the built-in lightweight shim +cmake -S . -B build -DTRX_USE_BOOST_FILESYSTEM=OFF +``` + +## Usage Examples + +All examples assume: + +``` +#include + +using namespace trxmmap; +``` + +### Read a TRX zip and inspect data + +``` +TrxFile *trx = load_from_zip("tracks.trx"); + +// Access streamlines: vertices are stored as an Eigen matrix +const auto num_vertices = trx->streamlines->_data.size() / 3; +const auto num_streamlines = trx->streamlines->_offsets.size() - 1; + +std::cout << "Vertices: " << num_vertices << "\n"; +std::cout << "Streamlines: " << num_streamlines << "\n"; +std::cout << "First vertex (x,y,z): " + << trx->streamlines->_data(0, 0) << "," + << trx->streamlines->_data(0, 1) << "," + << trx->streamlines->_data(0, 2) << "\n"; + +// Data-per-streamline and data-per-vertex are stored in maps +for (const auto &kv : trx->data_per_streamline) { + std::cout << "DPS '" << kv.first << "' elements: " + << kv.second->_matrix.size() << "\n"; +} +for (const auto &kv : trx->data_per_vertex) { + std::cout << "DPV '" << kv.first << "' elements: " + << kv.second->_data.size() << "\n"; +} + +trx->close(); // cleans up temporary on-disk data +delete trx; +``` + +### Read from an on-disk TRX directory + +``` +TrxFile *trx = load_from_directory("/path/to/trx_dir"); +std::cout << "Header JSON:\n" << trx->header.dump(2) << "\n"; +trx->close(); +delete trx; +``` + +### Write a TRX file + +You can modify a loaded `TrxFile` and save it to a new archive: + +``` +TrxFile *trx = load_from_zip("tracks.trx"); + +// Example: update header metadata +trx->header["COMMENT"] = "saved by trx-cpp"; + +// Save with no compression (ZIP_CM_STORE) or another libzip compression level +save(*trx, "tracks_copy.trx", ZIP_CM_STORE); + +trx->close(); +delete trx; +``` -### Installing -`cmake . && make` +### Notes on memory mapping -## How to use -COMING SOON +`TrxFile` uses memory-mapped arrays under the hood for large datasets. The +`close()` method cleans up any temporary folders created during zip extraction. +If you skip `close()`, temporary directories may be left behind. diff --git a/conanfile.py b/conanfile.py index 3cf80bb..5095267 100644 --- a/conanfile.py +++ b/conanfile.py @@ -18,11 +18,13 @@ class TrxCppConan(ConanFile): "shared": [True, False], "fPIC": [True, False], "with_tests": [True, False], + "with_boost": [True, False], } default_options = { "shared": False, "fPIC": True, "with_tests": False, + "with_boost": True, } generators = ("CMakeDeps",) exports_sources = ( @@ -42,6 +44,8 @@ def requirements(self): self.requires("nlohmann_json/3.11.3") self.requires("eigen/3.4.0") self.requires("spdlog/1.12.0") + if self.options.with_boost: + self.requires("boost/1.83.0") def build_requirements(self): if self.options.with_tests: @@ -65,6 +69,10 @@ def generate(self): tc = CMakeToolchain(self) mio_include = os.path.join(self.source_folder, "mio", "include") tc.variables["MIO_INCLUDE_DIR"] = mio_include + tc.variables["CMAKE_CXX_STANDARD"] = 11 + tc.variables["CMAKE_CXX_STANDARD_REQUIRED"] = "ON" + tc.variables["CMAKE_CXX_EXTENSIONS"] = "OFF" + tc.variables["TRX_USE_BOOST_FILESYSTEM"] = "ON" if self.options.with_boost else "OFF" tc.generate() def build(self): @@ -119,6 +127,11 @@ def package_info(self): "nlohmann_json::nlohmann_json", "eigen::Eigen3::Eigen", ] + if self.options.with_boost: + self.cpp_info.components["trx"].requires.extend([ + "boost::filesystem", + "boost::system", + ]) extra_includes = [] for dep_name in ("nlohmann_json", "eigen"): dep = self.dependencies.get(dep_name) diff --git a/include/trx/filesystem.h b/include/trx/filesystem.h new file mode 100644 index 0000000..a74da03 --- /dev/null +++ b/include/trx/filesystem.h @@ -0,0 +1,479 @@ +#ifndef TRX_FILESYSTEM_H +#define TRX_FILESYSTEM_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(TRX_USE_BOOST_FILESYSTEM) +#include +#include + +namespace trx +{ + namespace fs + { + using boost::filesystem::path; + + inline void _assign_error(std::error_code &out, const boost::system::error_code &in) + { + if (in) + out = std::error_code(in.value(), std::generic_category()); + else + out.clear(); + } + + inline bool exists(const path &p, std::error_code &ec) + { + boost::system::error_code bec; + bool ok = boost::filesystem::exists(p, bec); + _assign_error(ec, bec); + return ok; + } + + inline bool exists(const path &p) + { + boost::system::error_code bec; + bool ok = boost::filesystem::exists(p, bec); + if (bec) + throw std::runtime_error(bec.message()); + return ok; + } + + inline bool is_directory(const path &p, std::error_code &ec) + { + boost::system::error_code bec; + bool ok = boost::filesystem::is_directory(p, bec); + _assign_error(ec, bec); + return ok; + } + + inline bool create_directory(const path &p, std::error_code &ec) + { + boost::system::error_code bec; + bool ok = boost::filesystem::create_directory(p, bec); + _assign_error(ec, bec); + return ok; + } + + inline bool create_directories(const path &p, std::error_code &ec) + { + boost::system::error_code bec; + bool ok = boost::filesystem::create_directories(p, bec); + _assign_error(ec, bec); + return ok; + } + + inline path temp_directory_path(std::error_code &ec) + { + boost::system::error_code bec; + path p = boost::filesystem::temp_directory_path(bec); + _assign_error(ec, bec); + return p; + } + + inline std::uintmax_t remove_all(const path &p, std::error_code &ec) + { + boost::system::error_code bec; + std::uintmax_t count = boost::filesystem::remove_all(p, bec); + _assign_error(ec, bec); + return count; + } + } // namespace fs +} // namespace trx + +#else + +#include +#include +#include +#if defined(_WIN32) || defined(_WIN64) +#include +#endif + +namespace trx +{ + namespace fs + { + inline bool _is_separator(char c) + { + return c == '/' || c == '\\'; + } + + inline char _preferred_separator() + { +#if defined(_WIN32) || defined(_WIN64) + return '\\'; +#else + return '/'; +#endif + } + + class path + { + public: + path() {} + path(const std::string &p) : path_(p) {} + path(const char *p) : path_(p ? p : "") {} + + std::string string() const { return path_; } + const char *c_str() const { return path_.c_str(); } + bool empty() const { return path_.empty(); } + void clear() { path_.clear(); } + + bool is_absolute() const + { +#if defined(_WIN32) || defined(_WIN64) + if (path_.size() >= 2 && std::isalpha(path_[0]) && path_[1] == ':') + return (path_.size() >= 3 && _is_separator(path_[2])); + if (path_.size() >= 2 && _is_separator(path_[0]) && _is_separator(path_[1])) + return true; + return (!path_.empty() && _is_separator(path_[0])); +#else + return (!path_.empty() && path_[0] == '/'); +#endif + } + + bool has_parent_path() const + { + return !parent_path().empty(); + } + + path parent_path() const + { + if (path_.empty()) + return path(); + + size_t end = path_.size(); + while (end > 0 && _is_separator(path_[end - 1])) + --end; + if (end == 0) + return path(); + + size_t pos = path_.find_last_of("/\\", end - 1); + if (pos == std::string::npos) + return path(); + if (pos == 0) + return path(std::string(1, _preferred_separator())); +#if defined(_WIN32) || defined(_WIN64) + if (pos == 2 && std::isalpha(path_[0]) && path_[1] == ':') + return path(path_.substr(0, 3)); +#endif + return path(path_.substr(0, pos)); + } + + path lexically_normal() const + { + if (path_.empty()) + return path(); + + std::string root; + size_t start = 0; +#if defined(_WIN32) || defined(_WIN64) + if (path_.size() >= 2 && std::isalpha(path_[0]) && path_[1] == ':') + { + root = path_.substr(0, 2); + start = 2; + if (path_.size() >= 3 && _is_separator(path_[2])) + { + root += _preferred_separator(); + start = 3; + } + } + else if (path_.size() >= 2 && _is_separator(path_[0]) && _is_separator(path_[1])) + { + root = std::string(2, _preferred_separator()); + start = 2; + } + else if (!path_.empty() && _is_separator(path_[0])) + { + root = std::string(1, _preferred_separator()); + start = 1; + } +#else + if (!path_.empty() && path_[0] == '/') + { + root = "/"; + start = 1; + } +#endif + + std::vector parts; + size_t i = start; + while (i < path_.size()) + { + while (i < path_.size() && _is_separator(path_[i])) + ++i; + if (i >= path_.size()) + break; + size_t j = i; + while (j < path_.size() && !_is_separator(path_[j])) + ++j; + std::string part = path_.substr(i, j - i); + if (part == ".") + { + // skip + } + else if (part == "..") + { + if (!parts.empty() && parts.back() != "..") + { + parts.pop_back(); + } + else if (root.empty()) + { + parts.push_back(".."); + } + } + else + { + parts.push_back(part); + } + i = j; + } + + std::string out = root; + for (size_t idx = 0; idx < parts.size(); ++idx) + { + if (!out.empty() && !_is_separator(out[out.size() - 1])) + out += _preferred_separator(); + out += parts[idx]; + } + + if (out.empty() && root.empty()) + out = "."; + + return path(out); + } + + private: + std::string path_; + }; + + inline path operator/(const path &lhs, const path &rhs) + { + if (rhs.is_absolute()) + return rhs; + if (lhs.string().empty()) + return rhs; + std::string out = lhs.string(); + if (!_is_separator(out[out.size() - 1])) + out += _preferred_separator(); + out += rhs.string(); + return path(out); + } + + inline path operator/(const path &lhs, const char *rhs) + { + return lhs / path(rhs); + } + + inline path operator/(const path &lhs, const std::string &rhs) + { + return lhs / path(rhs); + } + + inline path operator/(const std::string &lhs, const path &rhs) + { + return path(lhs) / rhs; + } + + inline path operator/(const char *lhs, const path &rhs) + { + return path(lhs) / rhs; + } + + inline bool exists(const path &p, std::error_code &ec) + { + struct stat buf; + if (stat(p.c_str(), &buf) == 0) + { + ec.clear(); + return true; + } + if (errno == ENOENT) + { + ec.clear(); + return false; + } + ec = std::error_code(errno, std::generic_category()); + return false; + } + + inline bool exists(const path &p) + { + std::error_code ec; + bool ok = exists(p, ec); + if (ec) + throw std::runtime_error(ec.message()); + return ok; + } + + inline bool is_directory(const path &p, std::error_code &ec) + { + struct stat buf; + if (stat(p.c_str(), &buf) == 0) + { + ec.clear(); + return S_ISDIR(buf.st_mode); + } + if (errno == ENOENT) + { + ec.clear(); + return false; + } + ec = std::error_code(errno, std::generic_category()); + return false; + } + + inline bool create_directory(const path &p, std::error_code &ec) + { +#if defined(_WIN32) || defined(_WIN64) + int rc = _mkdir(p.c_str()); +#else + int rc = mkdir(p.c_str(), 0777); +#endif + if (rc == 0) + { + ec.clear(); + return true; + } + if (errno == EEXIST) + { + ec.clear(); + return false; + } + ec = std::error_code(errno, std::generic_category()); + return false; + } + + inline bool create_directories(const path &p, std::error_code &ec) + { + if (p.empty()) + { + ec.clear(); + return false; + } + + if (exists(p, ec)) + { + if (ec) + return false; + if (is_directory(p, ec)) + return false; + ec = std::make_error_code(std::errc::not_a_directory); + return false; + } + + path parent = p.parent_path(); + if (!parent.empty() && parent.string() != p.string()) + { + create_directories(parent, ec); + if (ec) + return false; + } + + return create_directory(p, ec); + } + + inline path temp_directory_path(std::error_code &ec) + { + const char *candidates[] = {std::getenv("TMPDIR"), std::getenv("TEMP"), std::getenv("TMP")}; + for (const char *candidate : candidates) + { + if (!candidate || std::string(candidate).empty()) + continue; + path p(candidate); + if (is_directory(p, ec)) + { + ec.clear(); + return p; + } + if (ec) + ec.clear(); + } + +#if defined(_WIN32) || defined(_WIN64) + path fallback("C:\\Temp"); +#else + path fallback("/tmp"); +#endif + if (is_directory(fallback, ec)) + { + ec.clear(); + return fallback; + } + if (!ec) + ec = std::make_error_code(std::errc::no_such_file_or_directory); + return path(); + } + + inline std::uintmax_t remove_all(const path &p, std::error_code &ec) + { + struct stat buf; + if (stat(p.c_str(), &buf) != 0) + { + if (errno == ENOENT) + { + ec.clear(); + return 0; + } + ec = std::error_code(errno, std::generic_category()); + return 0; + } + + std::uintmax_t count = 0; + if (S_ISDIR(buf.st_mode)) + { + DIR *dir = opendir(p.c_str()); + if (!dir) + { + ec = std::error_code(errno, std::generic_category()); + return count; + } + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) + { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + path child = path(p.string()) / entry->d_name; + count += remove_all(child, ec); + if (ec) + { + closedir(dir); + return count; + } + } + closedir(dir); + if (rmdir(p.c_str()) != 0) + { + ec = std::error_code(errno, std::generic_category()); + return count; + } + ++count; + } + else + { + if (unlink(p.c_str()) != 0) + { + ec = std::error_code(errno, std::generic_category()); + return count; + } + ++count; + } + + ec.clear(); + return count; + } + } // namespace fs +} // namespace trx + +#endif + +#endif diff --git a/include/trx/trx.h b/include/trx/trx.h index f4d0c79..14a5bc6 100644 --- a/include/trx/trx.h +++ b/include/trx/trx.h @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/include/trx/trx.tpp b/include/trx/trx.tpp index 730a8f0..554b23f 100644 --- a/include/trx/trx.tpp +++ b/include/trx/trx.tpp @@ -1109,11 +1109,11 @@ void save(TrxFile
&trx, const std::string filename, zip_uint32_t compression spdlog::error("Could not remove existing directory {}", filename); } } - std::filesystem::path dest_path(filename); + trx::fs::path dest_path(filename); if (dest_path.has_parent_path()) { std::error_code ec; - std::filesystem::create_directories(dest_path.parent_path(), ec); + trx::fs::create_directories(dest_path.parent_path(), ec); if (ec) { throw std::runtime_error("Could not create output parent directory: " + @@ -1125,8 +1125,8 @@ void save(TrxFile
&trx, const std::string filename, zip_uint32_t compression { throw std::runtime_error("Failed to create output directory: " + filename); } - const std::filesystem::path header_path = dest_path / "header.json"; - if (!std::filesystem::exists(header_path)) + const trx::fs::path header_path = dest_path / "header.json"; + if (!trx::fs::exists(header_path)) { throw std::runtime_error("Missing header.json in output directory: " + header_path.string()); } diff --git a/src/trx.cpp b/src/trx.cpp index 127cc07..b828d01 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -107,19 +107,20 @@ namespace trxmmap return ext; } -bool _is_path_within(const std::filesystem::path &child, const std::filesystem::path &parent) +bool _is_path_within(const trx::fs::path &child, const trx::fs::path &parent) { - auto parent_it = parent.begin(); - auto child_it = child.begin(); - - for (; parent_it != parent.end(); ++parent_it, ++child_it) - { - if (child_it == child.end() || *parent_it != *child_it) - { - return false; - } - } - return true; + std::string parent_str = parent.lexically_normal().string(); + std::string child_str = child.lexically_normal().string(); + if (parent_str.empty()) + return false; + if (child_str.size() < parent_str.size()) + return false; + if (child_str.compare(0, parent_str.size(), parent_str) != 0) + return false; + if (child_str.size() == parent_str.size()) + return true; + char next = child_str[parent_str.size()]; + return next == '/' || next == '\\'; } // TODO: check if there's a better way int _sizeof_dtype(std::string dtype) @@ -494,9 +495,9 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple } else { - std::filesystem::path env_path(val); + trx::fs::path env_path(val); std::error_code ec; - if (std::filesystem::exists(env_path, ec) && std::filesystem::is_directory(env_path, ec)) + if (trx::fs::exists(env_path, ec) && trx::fs::is_directory(env_path, ec)) { base_dir = env_path.string(); } @@ -512,9 +513,9 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple { continue; } - std::filesystem::path path(candidate); + trx::fs::path path(candidate); std::error_code ec; - if (std::filesystem::exists(path, ec) && std::filesystem::is_directory(path, ec)) + if (trx::fs::exists(path, ec) && trx::fs::is_directory(path, ec)) { base_dir = path.string(); break; @@ -524,7 +525,7 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple if (base_dir.empty()) { std::error_code ec; - auto sys_tmp = std::filesystem::temp_directory_path(ec); + auto sys_tmp = trx::fs::temp_directory_path(ec); if (!ec) { base_dir = sys_tmp.string(); @@ -535,7 +536,7 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple base_dir = "/tmp"; } - std::filesystem::path tmpl = std::filesystem::path(base_dir) / (prefix + "_XXXXXX"); + trx::fs::path tmpl = trx::fs::path(base_dir) / (prefix + "_XXXXXX"); std::string tmpl_str = tmpl.string(); std::vector buf(tmpl_str.begin(), tmpl_str.end()); buf.push_back('\0'); @@ -554,7 +555,7 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple throw std::invalid_argument("Zip archive pointer is null"); } std::string root_dir = make_temp_dir("trx_zip"); - std::filesystem::path normalized_root = std::filesystem::path(root_dir).lexically_normal(); + trx::fs::path normalized_root = trx::fs::path(root_dir).lexically_normal(); zip_int64_t num_entries = zip_get_num_entries(zfolder, ZIP_FL_UNCHANGED); for (zip_int64_t i = 0; i < num_entries; ++i) @@ -566,15 +567,15 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple } std::string entry(entry_name); - std::filesystem::path entry_path(entry); + trx::fs::path entry_path(entry); if (entry_path.is_absolute()) { throw std::runtime_error("Zip entry has absolute path: " + entry); } - std::filesystem::path normalized_entry = entry_path.lexically_normal(); - std::filesystem::path out_path = normalized_root / normalized_entry; - std::filesystem::path normalized_out = out_path.lexically_normal(); + trx::fs::path normalized_entry = entry_path.lexically_normal(); + trx::fs::path out_path = normalized_root / normalized_entry; + trx::fs::path normalized_out = out_path.lexically_normal(); if (!_is_path_within(normalized_out, normalized_root)) { @@ -584,7 +585,7 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple if (!entry.empty() && entry.back() == '/') { std::error_code ec; - std::filesystem::create_directories(normalized_out, ec); + trx::fs::create_directories(normalized_out, ec); if (ec) { throw std::runtime_error("Failed to create directory: " + normalized_out.string()); @@ -593,7 +594,7 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple } std::error_code ec; - std::filesystem::create_directories(normalized_out.parent_path(), ec); + trx::fs::create_directories(normalized_out.parent_path(), ec); if (ec) { throw std::runtime_error("Failed to create parent directory: " + normalized_out.parent_path().string()); @@ -605,7 +606,7 @@ mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple throw std::runtime_error("Failed to open zip entry: " + entry); } - std::ofstream out(normalized_out, std::ios::binary); + std::ofstream out(normalized_out.string(), std::ios::binary); if (!out.is_open()) { zip_fclose(zf); diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index d463faa..40eae48 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(trx_cpp_test_package LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(trx-cpp CONFIG REQUIRED) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 56fb1af..6b83f3f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ endif() add_executable(test_mmap test_trx_mmap.cpp) target_link_libraries(test_mmap PRIVATE trx GTest::gtest GTest::gtest_main) +target_compile_features(test_mmap PRIVATE cxx_std_14) include(GoogleTest) gtest_discover_tests(test_mmap) diff --git a/tests/test_trx_mmap.cpp b/tests/test_trx_mmap.cpp index 30b13da..f7cb2d9 100644 --- a/tests/test_trx_mmap.cpp +++ b/tests/test_trx_mmap.cpp @@ -4,18 +4,19 @@ #include #include #include -#include +#include #include #include using namespace Eigen; using namespace trxmmap; +namespace fs = trx::fs; namespace { struct TestTrxFixture { - std::filesystem::path root_dir; + fs::path root_dir; std::string path; std::string dir_path; json expected_header; @@ -27,7 +28,7 @@ namespace std::error_code ec; if (!root_dir.empty()) { - std::filesystem::remove_all(root_dir, ec); + fs::remove_all(root_dir, ec); if (ec) { std::cerr << "Failed to clean up test directory " << root_dir.string() @@ -38,10 +39,10 @@ namespace } }; - std::filesystem::path make_temp_test_dir(const std::string &prefix) + fs::path make_temp_test_dir(const std::string &prefix) { std::error_code ec; - auto base = std::filesystem::temp_directory_path(ec); + auto base = fs::temp_directory_path(ec); if (ec) { throw std::runtime_error("Failed to get temp directory: " + ec.message()); @@ -52,9 +53,9 @@ namespace for (int attempt = 0; attempt < 100; ++attempt) { - std::filesystem::path candidate = base / (prefix + "_" + std::to_string(dist(rng))); + fs::path candidate = base / (prefix + "_" + std::to_string(dist(rng))); std::error_code dir_ec; - if (std::filesystem::create_directory(candidate, dir_ec)) + if (fs::create_directory(candidate, dir_ec)) { return candidate; } @@ -70,10 +71,10 @@ namespace { TestTrxFixture fixture; - std::filesystem::path root_dir = make_temp_test_dir("trx_test"); - std::filesystem::path trx_dir = root_dir / "trx_data"; + fs::path root_dir = make_temp_test_dir("trx_test"); + fs::path trx_dir = root_dir / "trx_data"; std::error_code ec; - if (!std::filesystem::create_directory(trx_dir, ec) && ec) + if (!fs::create_directory(trx_dir, ec) && ec) { throw std::runtime_error("Failed to create trx data directory: " + ec.message()); } @@ -93,8 +94,8 @@ namespace {0.0, 0.0, 0.0, 1.0}}; // Write header.json - std::filesystem::path header_path = trx_dir / "header.json"; - std::ofstream header_out(header_path); + fs::path header_path = trx_dir / "header.json"; + std::ofstream header_out(header_path.string()); if (!header_out.is_open()) { throw std::runtime_error("Failed to write header.json"); @@ -105,7 +106,7 @@ namespace // Write positions (float16) Matrix positions(fixture.nb_vertices, 3); positions.setZero(); - std::filesystem::path positions_path = trx_dir / "positions.3.float16"; + fs::path positions_path = trx_dir / "positions.3.float16"; trxmmap::write_binary(positions_path.c_str(), positions); struct stat sb; if (stat(positions_path.c_str(), &sb) != 0) @@ -126,7 +127,7 @@ namespace } offsets(fixture.nb_streamlines, 0) = static_cast(fixture.nb_vertices); - std::filesystem::path offsets_path = trx_dir / "offsets.uint64"; + fs::path offsets_path = trx_dir / "offsets.uint64"; trxmmap::write_binary(offsets_path.c_str(), offsets); if (stat(offsets_path.c_str(), &sb) != 0) { @@ -349,8 +350,8 @@ TEST(TrxFileMemmap, __dichotomic_search) TEST(TrxFileMemmap, __create_memmap) { - std::filesystem::path dir = make_temp_test_dir("trx_memmap"); - std::filesystem::path path = dir / "offsets.int16"; + fs::path dir = make_temp_test_dir("trx_memmap"); + fs::path path = dir / "offsets.int16"; std::tuple shape = std::make_tuple(3, 4); @@ -375,13 +376,13 @@ TEST(TrxFileMemmap, __create_memmap) EXPECT_EQ(expected_m, real_m); std::error_code ec; - std::filesystem::remove_all(dir, ec); + fs::remove_all(dir, ec); } TEST(TrxFileMemmap, __create_memmap_empty) { - std::filesystem::path dir = make_temp_test_dir("trx_memmap_empty"); - std::filesystem::path path = dir / "empty.float32"; + fs::path dir = make_temp_test_dir("trx_memmap_empty"); + fs::path path = dir / "empty.float32"; std::tuple shape = std::make_tuple(0, 1); mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path.string(), shape); @@ -392,7 +393,7 @@ TEST(TrxFileMemmap, __create_memmap_empty) EXPECT_EQ(empty_mmap.size(), 0u); std::error_code ec; - std::filesystem::remove_all(dir, ec); + fs::remove_all(dir, ec); } TEST(TrxFileMemmap, load_header)