From 39c0d953db11fce3def2e714ad808c638ea7a7a2 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:25:40 -0500 Subject: [PATCH 01/19] update things --- CMakeLists.txt | 9 +- src/trx.cpp | 96 ++++++++++++++++++- src/trx.h | 4 + src/trx.tpp | 147 +++++++++++----------------- tests/CMakeLists.txt | 55 ++++++----- tests/test_trx_mmap.cpp | 208 ++++++++++++++++++++++++++++++++++------ 6 files changed, 370 insertions(+), 149 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b3a8300..f63483c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,8 @@ -cmake_minimum_required(VERSION 3.0.0) +cmake_minimum_required(VERSION 3.10) cmake_policy(SET CMP0074 NEW) cmake_policy(SET CMP0079 NEW) project(trx VERSION 0.1.0) -set (CMAKE_CXX_STANDARD 11) +set (CMAKE_CXX_STANDARD 17) #set(CMAKE_BUILD_TYPE RelWithDebInfo) set(CMAKE_BUILD_TYPE Debug) @@ -11,6 +11,10 @@ find_package(libzip REQUIRED) find_package (Eigen3 CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) +find_path(MIO_INCLUDE_DIR mio/mmap.hpp PATH_SUFFIXES include) +if (NOT MIO_INCLUDE_DIR) + message(FATAL_ERROR "mio headers not found. Set MIO_INCLUDE_DIR to the folder containing mio/mmap.hpp.") +endif() add_library(trx src/trx.cpp src/trx.tpp src/trx.h) @@ -22,6 +26,7 @@ TARGET_LINK_LIBRARIES(trx spdlog::spdlog spdlog::spdlog_header_only ) +target_include_directories(trx PRIVATE ${MIO_INCLUDE_DIR}) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) diff --git a/src/trx.cpp b/src/trx.cpp index e65ff2d..39fda9c 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #define SYSERROR() errno //#define ZIP_DD_SIG 0x08074b50 @@ -159,6 +160,8 @@ namespace trxmmap return "uint32"; case 'm': return "uint64"; + case 'y': // unsigned long long (Itanium ABI) + return "uint64"; case 'a': return "int8"; case 's': @@ -167,6 +170,8 @@ namespace trxmmap return "int32"; case 'l': return "int64"; + case 'x': // long long (Itanium ABI) + return "int64"; case 'f': return "float32"; case 'd': @@ -228,8 +233,16 @@ namespace trxmmap json load_header(zip_t *zfolder) { + if (zfolder == nullptr) + { + throw std::invalid_argument("Zip archive pointer is null"); + } // load file zip_file_t *zh = zip_fopen(zfolder, "header.json", ZIP_FL_UNCHANGED); + if (zh == nullptr) + { + throw std::runtime_error("Failed to open header.json in zip archive"); + } // read data from file in chunks of 255 characters until data is fully loaded int buff_len = 255 * sizeof(char); @@ -245,7 +258,7 @@ namespace trxmmap } } - free(zh); + zip_fclose(zh); free(buffer); // convert jstream data into Json. @@ -287,6 +300,11 @@ namespace trxmmap allocate_file(filename, filesize); } + if (filesize == 0) + { + return mio::shared_mmap_sink(); + } + // std::error_code error; mio::shared_mmap_sink rw_mmap(filename, offset, filesize); @@ -446,6 +464,75 @@ namespace trxmmap return rmdir(d); } + std::string extract_zip_to_directory(zip_t *zfolder) + { + if (zfolder == nullptr) + { + throw std::invalid_argument("Zip archive pointer is null"); + } + char t[] = "/tmp/trx_zip_XXXXXX"; + char *dirname = mkdtemp(t); + if (dirname == nullptr) + { + throw std::runtime_error("Failed to create temporary directory for zip extraction"); + } + std::string root_dir(dirname); + + zip_int64_t num_entries = zip_get_num_entries(zfolder, ZIP_FL_UNCHANGED); + for (zip_int64_t i = 0; i < num_entries; ++i) + { + const char *entry_name = zip_get_name(zfolder, i, ZIP_FL_UNCHANGED); + if (entry_name == nullptr) + { + continue; + } + std::string entry(entry_name); + std::filesystem::path out_path = std::filesystem::path(root_dir) / entry; + + if (!entry.empty() && entry.back() == '/') + { + std::error_code ec; + std::filesystem::create_directories(out_path, ec); + if (ec) + { + throw std::runtime_error("Failed to create directory: " + out_path.string()); + } + continue; + } + + std::error_code ec; + std::filesystem::create_directories(out_path.parent_path(), ec); + if (ec) + { + throw std::runtime_error("Failed to create parent directory: " + out_path.parent_path().string()); + } + + zip_file_t *zf = zip_fopen_index(zfolder, i, ZIP_FL_UNCHANGED); + if (zf == nullptr) + { + throw std::runtime_error("Failed to open zip entry: " + entry); + } + + std::ofstream out(out_path, std::ios::binary); + if (!out.is_open()) + { + zip_fclose(zf); + throw std::runtime_error("Failed to open output file: " + out_path.string()); + } + + char buffer[4096]; + zip_int64_t nbytes = 0; + while ((nbytes = zip_fread(zf, buffer, sizeof(buffer))) > 0) + { + out.write(buffer, nbytes); + } + out.close(); + zip_fclose(zf); + } + + return root_dir; + } + void zip_from_folder(zip_t *zf, const std::string root, const std::string directory, zip_uint32_t compression_standard) { DIR *dir; @@ -478,12 +565,17 @@ namespace trxmmap zip_source_t *s; + zip_int64_t file_idx = -1; if ((s = zip_source_file(zf, fullpath, 0, 0)) == NULL || - zip_file_add(zf, fn.c_str(), s, ZIP_FL_ENC_UTF_8) < 0) + (file_idx = zip_file_add(zf, fn.c_str(), s, ZIP_FL_ENC_UTF_8)) < 0) { zip_source_free(s); spdlog::error("error adding file {}: {}", fn, zip_strerror(zf)); } + else if (zip_set_file_compression(zf, file_idx, compression_standard, 0) < 0) + { + spdlog::error("error setting compression for {}: {}", fn, zip_strerror(zf)); + } } } closedir(dir); diff --git a/src/trx.h b/src/trx.h index 4cf112a..56a09c6 100644 --- a/src/trx.h +++ b/src/trx.h @@ -13,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include @@ -289,6 +292,7 @@ namespace trxmmap void copy_dir(const char *src, const char *dst); void copy_file(const char *src, const char *dst); int rm_dir(const char *d); + std::string extract_zip_to_directory(zip_t *zfolder); std::string rm_root(std::string root, const std::string path); #include "trx.tpp" diff --git a/src/trx.tpp b/src/trx.tpp index 92eebdc..8b6426c 100644 --- a/src/trx.tpp +++ b/src/trx.tpp @@ -60,17 +60,11 @@ std::string _generate_filename_from_data(const MatrixBase
&arr, std::string std::string new_filename; if (n_cols == 1) { - int buffsize = filename.size() + dt.size() + 2; - char buff[buffsize]; - snprintf(buff, sizeof(buff), "%s.%s", base.c_str(), dt.c_str()); - new_filename = buff; + new_filename = base + "." + dt; } else { - int buffsize = filename.size() + dt.size() + n_cols + 3; - char buff[buffsize]; - snprintf(buff, sizeof(buff), "%s.%i.%s", base.c_str(), n_cols, dt.c_str()); - new_filename = buff; + new_filename = base + "." + std::to_string(n_cols) + "." + dt; } return new_filename; @@ -87,7 +81,7 @@ Matrix _compute_lengths(const MatrixBase
&offsets, int if (last_elem_pos == offsets.size() - 1) { Matrix tmp(offsets.template cast()); - ediff1d(lengths, tmp, uint32_t(nb_vertices - offsets(last))); + ediff1d(lengths, tmp, uint32_t(nb_vertices - offsets(offsets.size() - 1))); } else { @@ -483,8 +477,10 @@ TrxFile
*TrxFile
::_create_trx_from_pointer(json header, std::mapheader["NB_STREAMLINES"]) || dim != 1) { - - throw std::invalid_argument("Wrong offsets size/dimensionality"); + throw std::invalid_argument("Wrong offsets size/dimensionality: size=" + + std::to_string(size) + " nb_streamlines=" + + std::to_string(int(trx->header["NB_STREAMLINES"])) + + " dim=" + std::to_string(dim) + " filename=" + elem_filename); } std::tuple shape = std::make_tuple(trx->header["NB_STREAMLINES"], 1); @@ -812,20 +808,20 @@ std::tuple TrxFile
::_copy_fixed_arrays_from(TrxFile
*trx, int if (curr_pts_len == 0) return std::make_tuple(strs_start, pts_start); - this->streamlines->_data(seq(pts_start, pts_end - 1), all) = trx->streamlines->_data(seq(0, curr_pts_len - 1), all); - this->streamlines->_offsets(seq(strs_start, strs_end - 1), all) = (trx->streamlines->_offsets(seq(0, curr_strs_len - 1), all).array() + pts_start).matrix(); - this->streamlines->_lengths(seq(strs_start, strs_end - 1), all) = trx->streamlines->_lengths(seq(0, curr_strs_len - 1), all); + this->streamlines->_data(seq(pts_start, pts_end - 1), Eigen::placeholders::all) = trx->streamlines->_data(seq(0, curr_pts_len - 1), Eigen::placeholders::all); + this->streamlines->_offsets(seq(strs_start, strs_end - 1), Eigen::placeholders::all) = (trx->streamlines->_offsets(seq(0, curr_strs_len - 1), Eigen::placeholders::all).array() + pts_start).matrix(); + this->streamlines->_lengths(seq(strs_start, strs_end - 1), Eigen::placeholders::all) = trx->streamlines->_lengths(seq(0, curr_strs_len - 1), Eigen::placeholders::all); for (auto const &x : this->data_per_vertex) { - this->data_per_vertex[x.first]->_data(seq(pts_start, pts_end - 1), all) = trx->data_per_vertex[x.first]->_data(seq(0, curr_pts_len - 1), all); + this->data_per_vertex[x.first]->_data(seq(pts_start, pts_end - 1), Eigen::placeholders::all) = trx->data_per_vertex[x.first]->_data(seq(0, curr_pts_len - 1), Eigen::placeholders::all); new (&(this->data_per_vertex[x.first]->_offsets)) Map>(trx->data_per_vertex[x.first]->_offsets.data(), trx->data_per_vertex[x.first]->_offsets.rows(), trx->data_per_vertex[x.first]->_offsets.cols()); this->data_per_vertex[x.first]->_lengths = trx->data_per_vertex[x.first]->_lengths; } for (auto const &x : this->data_per_streamline) { - this->data_per_streamline[x.first]->_matrix(seq(strs_start, strs_end - 1), all) = trx->data_per_streamline[x.first]->_matrix(seq(0, curr_strs_len - 1), all); + this->data_per_streamline[x.first]->_matrix(seq(strs_start, strs_end - 1), Eigen::placeholders::all) = trx->data_per_streamline[x.first]->_matrix(seq(0, curr_strs_len - 1), Eigen::placeholders::all); } return std::make_tuple(strs_end, pts_end); @@ -863,7 +859,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) if (nb_vertices == -1) { - ptrs_end = this->streamlines->_lengths(all, 0).sum(); + ptrs_end = this->streamlines->_lengths(Eigen::placeholders::all, 0).sum(); nb_vertices = ptrs_end; } else if (nb_vertices < ptrs_end) @@ -886,7 +882,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) TrxFile
*trx = _initialize_empty_trx(nb_streamlines, nb_vertices, this); spdlog::info("Resizing streamlines from size {} to {}", this->streamlines->_lengths.size(), nb_streamlines); - spdlog::info("Resizing vertices from size {} to {}", this->streamlines->_data(all, 0).size(), nb_vertices); + spdlog::info("Resizing vertices from size {} to {}", this->streamlines->_data(Eigen::placeholders::all, 0).size(), nb_vertices); if (nb_streamlines < this->header["NB_STREAMLINES"]) trx->_copy_fixed_arrays_from(this, -1, -1, nb_streamlines); @@ -920,7 +916,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) { for (int j = 0; j < x.second->_matrix.cols(); ++j) { - if (x.second->_matrix(i, j) < strs_end) + if (static_cast(x.second->_matrix(i, j)) < strs_end) { keep_rows.push_back(i); } @@ -1014,87 +1010,28 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) template TrxFile
*load_from_zip(std::string filename) { - // TODO: check error values - int *errorp; - zip_t *zf = zip_open(filename.c_str(), 0, errorp); - json header = load_header(zf); - - std::map> file_pointer_size; - long long global_pos = 0; - long long mem_address = 0; - - int num_entries = zip_get_num_entries(zf, ZIP_FL_UNCHANGED); - - for (int i = 0; i < num_entries; ++i) + int errorp = 0; + zip_t *zf = zip_open(filename.c_str(), 0, &errorp); + if (zf == nullptr) { - std::string elem_filename = zip_get_name(zf, i, ZIP_FL_UNCHANGED); - - zip_stat_t sb; - zip_file_t *zft; - - if (zip_stat(zf, elem_filename.c_str(), ZIP_FL_UNCHANGED, &sb) != 0) - { - return NULL; - } - - global_pos += 30 + elem_filename.size(); - - size_t lastdot = elem_filename.find_last_of("."); - - if (lastdot == std::string::npos) - { - global_pos += sb.comp_size; - continue; - } - std::string ext = elem_filename.substr(lastdot + 1, std::string::npos); - - // apparently all zip directory names end with a slash. may be a better way - if (ext.compare("json") == 0 || elem_filename.rfind("/") == elem_filename.size() - 1) - { - global_pos += sb.comp_size; - continue; - } - - if (!_is_dtype_valid(ext)) - { - global_pos += sb.comp_size; - continue; - // maybe throw error here instead? - // throw std::invalid_argument("The dtype is not supported"); - } - - if (ext.compare("bit") == 0) - { - ext = "bool"; - } - - // get file stats - - // std::ifstream file(filename, std::ios::binary); - // file.seekg(global_pos); - - // unsigned char signature[4] = {0}; - // const unsigned char local_sig[4] = {0x50, 0x4b, 0x03, 0x04}; - // file.read((char *)signature, sizeof(signature)); + throw std::runtime_error("Could not open zip file: " + filename); + } - // if (memcmp(signature, local_sig, sizeof(signature)) == 0) - // { - // global_pos += 30; - // // global_pos += sb.comp_size + elem_filename.size(); - // } + std::string temp_dir = extract_zip_to_directory(zf); + zip_close(zf); - long long size = sb.size / _sizeof_dtype(ext); - mem_address = global_pos; - file_pointer_size[elem_filename] = {mem_address, size}; - global_pos += sb.comp_size; - } - return TrxFile
::_create_trx_from_pointer(header, file_pointer_size, filename); + return load_from_directory
(temp_dir); } template TrxFile
*load_from_directory(std::string path) { - std::string directory = (std::string)canonicalize_file_name(path.c_str()); + std::string directory = path; + char resolved[PATH_MAX]; + if (realpath(path.c_str(), resolved) != nullptr) + { + directory = resolved; + } std::string header_name = directory + SEPARATOR + "header.json"; // TODO: add check to verify that it's open @@ -1144,6 +1081,12 @@ void save(TrxFile
&trx, const std::string filename, zip_uint32_t compression { struct stat sb; + struct stat tmp_sb; + if (stat(tmp_dir_name.c_str(), &tmp_sb) != 0 || !S_ISDIR(tmp_sb.st_mode)) + { + throw std::runtime_error("Temporary TRX directory does not exist: " + tmp_dir_name); + } + if (stat(filename.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)) { if (rm_dir(filename.c_str()) != 0) @@ -1151,7 +1094,27 @@ 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); + if (dest_path.has_parent_path()) + { + std::error_code ec; + std::filesystem::create_directories(dest_path.parent_path(), ec); + if (ec) + { + throw std::runtime_error("Could not create output parent directory: " + + dest_path.parent_path().string()); + } + } copy_dir(tmp_dir_name.c_str(), filename.c_str()); + if (stat(filename.c_str(), &sb) != 0 || !S_ISDIR(sb.st_mode)) + { + 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)) + { + throw std::runtime_error("Missing header.json in output directory: " + header_path.string()); + } copy_trx->close(); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 847dc83..3390e30 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,8 +1,8 @@ -cmake_minimum_required(VERSION 3.0.0) +cmake_minimum_required(VERSION 3.10) cmake_policy(SET CMP0074 NEW) cmake_policy(SET CMP0079 NEW) project(trx) -set (CMAKE_CXX_STANDARD 11) +set (CMAKE_CXX_STANDARD 17) set(PROJECT_BINARY_DIR ../../builds) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/build/tests) @@ -15,27 +15,38 @@ find_package(libzip REQUIRED) find_package (Eigen3 CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) -find_package(GTest CONFIG REQUIRED) - -enable_testing() - -include_directories(../src) -add_executable(test_mmap test_trx_mmap.cpp ../src/trx.h ../src/trx.tpp ../src/trx.cpp) - - -TARGET_LINK_LIBRARIES(test_mmap - nlohmann_json::nlohmann_json - Eigen3::Eigen - libzip::zip - GTest::gtest - GTest::gtest_main - spdlog::spdlog - spdlog::spdlog_header_only -) +option(TRX_ENABLE_TESTS "Build trx-cpp tests" ON) +find_package(GTest CONFIG QUIET) +if (TRX_ENABLE_TESTS AND NOT GTest_FOUND) + message(FATAL_ERROR "GTest not found. Set GTest_DIR/CMAKE_PREFIX_PATH or pass -DTRX_ENABLE_TESTS=OFF to skip tests.") +endif() +find_path(MIO_INCLUDE_DIR mio/mmap.hpp PATH_SUFFIXES include) +if (NOT MIO_INCLUDE_DIR) + message(FATAL_ERROR "mio headers not found. Set MIO_INCLUDE_DIR to the folder containing mio/mmap.hpp.") +endif() + +if (TRX_ENABLE_TESTS AND GTest_FOUND) + enable_testing() + + include_directories(../src) + add_executable(test_mmap test_trx_mmap.cpp ../src/trx.h ../src/trx.tpp ../src/trx.cpp) + + + TARGET_LINK_LIBRARIES(test_mmap + nlohmann_json::nlohmann_json + Eigen3::Eigen + libzip::zip + GTest::gtest + GTest::gtest_main + spdlog::spdlog + spdlog::spdlog_header_only + ) + target_include_directories(test_mmap PRIVATE ${MIO_INCLUDE_DIR}) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) -include(CPack) -include(GoogleTest) -gtest_discover_tests(test_mmap) + include(CPack) + include(GoogleTest) + gtest_discover_tests(test_mmap) +endif() diff --git a/tests/test_trx_mmap.cpp b/tests/test_trx_mmap.cpp index d381731..49ae46c 100644 --- a/tests/test_trx_mmap.cpp +++ b/tests/test_trx_mmap.cpp @@ -1,10 +1,141 @@ #include #include "../src/trx.h" #include +#include +#include using namespace Eigen; using namespace trxmmap; +namespace +{ + struct TestTrxFixture + { + std::string path; + std::string dir_path; + json expected_header; + int nb_vertices; + int nb_streamlines; + }; + + TestTrxFixture create_fixture() + { + char t[] = "/tmp/trx_test_XXXXXX"; + char *dirname = mkdtemp(t); + if (dirname == nullptr) + { + throw std::runtime_error("Failed to create temporary directory"); + } + + TestTrxFixture fixture; + std::string root_dir(dirname); + std::string trx_dir = root_dir + "/trx_data"; + if (mkdir(trx_dir.c_str(), S_IRWXU) != 0) + { + throw std::runtime_error("Failed to create trx data directory"); + } + fixture.path = root_dir + "/small.trx"; + fixture.dir_path = trx_dir; + fixture.nb_vertices = 12; + fixture.nb_streamlines = 4; + + fixture.expected_header["DIMENSIONS"] = {117, 151, 115}; + fixture.expected_header["NB_STREAMLINES"] = fixture.nb_streamlines; + fixture.expected_header["NB_VERTICES"] = fixture.nb_vertices; + fixture.expected_header["VOXEL_TO_RASMM"] = {{-1.25, 0.0, 0.0, 72.5}, + {0.0, 1.25, 0.0, -109.75}, + {0.0, 0.0, 1.25, -64.5}, + {0.0, 0.0, 0.0, 1.0}}; + + // Write header.json + std::string header_path = trx_dir + "/header.json"; + std::ofstream header_out(header_path); + if (!header_out.is_open()) + { + throw std::runtime_error("Failed to write header.json"); + } + header_out << std::setw(4) << fixture.expected_header << std::endl; + header_out.close(); + + // Write positions (float16) + Matrix positions(fixture.nb_vertices, 3); + positions.setZero(); + std::string 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) + { + throw std::runtime_error("Failed to stat positions file"); + } + const size_t expected_positions_bytes = fixture.nb_vertices * 3 * sizeof(half); + if (static_cast(sb.st_size) != expected_positions_bytes) + { + throw std::runtime_error("Positions file size mismatch"); + } + + // Write offsets (uint64) + Matrix offsets(fixture.nb_streamlines, 1); + for (int i = 0; i < fixture.nb_streamlines; ++i) + { + offsets(i, 0) = static_cast(i * (fixture.nb_vertices / fixture.nb_streamlines)); + } + std::string offsets_path = trx_dir + "/offsets.uint64"; + trxmmap::write_binary(offsets_path.c_str(), offsets); + if (stat(offsets_path.c_str(), &sb) != 0) + { + throw std::runtime_error("Failed to stat offsets file"); + } + const size_t expected_offsets_bytes = fixture.nb_streamlines * sizeof(uint64_t); + if (static_cast(sb.st_size) != expected_offsets_bytes) + { + throw std::runtime_error("Offsets file size mismatch"); + } + + // Zip the directory into a trx file without compression + int errorp = 0; + zip_t *zf = zip_open(fixture.path.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &errorp); + if (zf == nullptr) + { + throw std::runtime_error("Failed to create trx zip file"); + } + trxmmap::zip_from_folder(zf, trx_dir, trx_dir, ZIP_CM_STORE); + if (zip_close(zf) != 0) + { + throw std::runtime_error("Failed to close trx zip file"); + } + + // Validate zip entry sizes + int zip_err = 0; + zip_t *verify_zip = zip_open(fixture.path.c_str(), 0, &zip_err); + if (verify_zip == nullptr) + { + throw std::runtime_error("Failed to reopen trx zip file"); + } + zip_stat_t stat_buf; + if (zip_stat(verify_zip, "offsets.uint64", ZIP_FL_UNCHANGED, &stat_buf) != 0 || + static_cast(stat_buf.size) != expected_offsets_bytes) + { + zip_close(verify_zip); + throw std::runtime_error("Zip offsets entry size mismatch"); + } + if (zip_stat(verify_zip, "positions.3.float16", ZIP_FL_UNCHANGED, &stat_buf) != 0 || + static_cast(stat_buf.size) != expected_positions_bytes) + { + zip_close(verify_zip); + throw std::runtime_error("Zip positions entry size mismatch"); + } + zip_close(verify_zip); + + return fixture; + } + + const TestTrxFixture &get_fixture() + { + static TestTrxFixture fixture = create_fixture(); + return fixture; + } +} + // TODO: Test null filenames. Maybe use MatrixBase instead of ArrayBase // TODO: try to update test case to use GTest parameterization TEST(TrxFileMemmap, __generate_filename_from_data) @@ -202,31 +333,34 @@ TEST(TrxFileMemmap, __create_memmap) EXPECT_EQ(expected_m, real_m); } -TEST(TrxFileMemmap, load_header) +TEST(TrxFileMemmap, __create_memmap_empty) { - std::string path = "data/small.trx"; - int *errorp; - zip_t *zf = zip_open(path.c_str(), 0, errorp); - json root = trxmmap::load_header(zf); + char t[] = "/tmp/trx_XXXXXX"; + char *dirname = mkdtemp(t); - // expected output - json expected; + std::string path(dirname); + path += "/empty.float32"; - expected["DIMENSIONS"] = {117, 151, 115}; - expected["NB_STREAMLINES"] = 1000; - expected["NB_VERTICES"] = 33886; - expected["VOXEL_TO_RASMM"] = {{-1.25, 0.0, 0.0, 72.5}, - {0.0, 1.25, 0.0, -109.75}, - {0.0, 0.0, 1.25, -64.5}, - {0.0, 0.0, 0.0, 1.0}}; + std::tuple shape = std::make_tuple(0, 1); + mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path, shape); - EXPECT_EQ(root, expected); + struct stat sb; + ASSERT_EQ(stat(path.c_str(), &sb), 0); + EXPECT_EQ(sb.st_size, 0); + EXPECT_EQ(empty_mmap.size(), 0u); +} - std::string expected_str = "{\"DIMENSIONS\":[117,151,115],\"NB_STREAMLINES\":1000,\"NB_VERTICES\":33886,\"VOXEL_TO_RASMM\":[[-1.25,0.0,0.0,72.5],[0.0,1.25,0.0,-109.75],[0.0,0.0,1.25,-64.5],[0.0,0.0,0.0,1.0]]}"; +TEST(TrxFileMemmap, load_header) +{ + const auto &fixture = get_fixture(); + int errorp = 0; + zip_t *zf = zip_open(fixture.path.c_str(), 0, &errorp); + json root = trxmmap::load_header(zf); - EXPECT_EQ(root.dump(), expected_str); + EXPECT_EQ(root, fixture.expected_header); + EXPECT_EQ(root.dump(), fixture.expected_header.dump()); - free(zf); + zip_close(zf); } // TEST(TrxFileMemmap, _load) @@ -247,7 +381,15 @@ TEST(TrxFileMemmap, load_header) TEST(TrxFileMemmap, load_zip) { - trxmmap::TrxFile *trx = trxmmap::load_from_zip("data/small.trx"); + const auto &fixture = get_fixture(); + trxmmap::TrxFile *trx = trxmmap::load_from_zip(fixture.path); + EXPECT_GT(trx->streamlines->_data.size(), 0); +} + +TEST(TrxFileMemmap, load_directory) +{ + const auto &fixture = get_fixture(); + trxmmap::TrxFile *trx = trxmmap::load_from_directory(fixture.dir_path); EXPECT_GT(trx->streamlines->_data.size(), 0); } @@ -268,35 +410,37 @@ TEST(TrxFileMemmap, TrxFile) EXPECT_EQ(trx->header, expected); - std::string path = "data/small.trx"; - int *errorp; - zip_t *zf = zip_open(path.c_str(), 0, errorp); + const auto &fixture = get_fixture(); + int errorp = 0; + zip_t *zf = zip_open(fixture.path.c_str(), 0, &errorp); json root = trxmmap::load_header(zf); TrxFile *root_init = new TrxFile(); root_init->header = root; + zip_close(zf); // TODO: test for now.. - trxmmap::TrxFile *trx_init = new TrxFile(33886, 1000, root_init); + trxmmap::TrxFile *trx_init = new TrxFile(fixture.nb_vertices, fixture.nb_streamlines, root_init); json init_as; init_as["DIMENSIONS"] = {117, 151, 115}; - init_as["NB_STREAMLINES"] = 1000; - init_as["NB_VERTICES"] = 33886; + init_as["NB_STREAMLINES"] = fixture.nb_streamlines; + init_as["NB_VERTICES"] = fixture.nb_vertices; init_as["VOXEL_TO_RASMM"] = {{-1.25, 0.0, 0.0, 72.5}, {0.0, 1.25, 0.0, -109.75}, {0.0, 0.0, 1.25, -64.5}, {0.0, 0.0, 0.0, 1.0}}; EXPECT_EQ(root_init->header, init_as); - EXPECT_EQ(trx_init->streamlines->_data.size(), 33886 * 3); - EXPECT_EQ(trx_init->streamlines->_offsets.size(), 1000); - EXPECT_EQ(trx_init->streamlines->_lengths.size(), 1000); + EXPECT_EQ(trx_init->streamlines->_data.size(), fixture.nb_vertices * 3); + EXPECT_EQ(trx_init->streamlines->_offsets.size(), fixture.nb_streamlines); + EXPECT_EQ(trx_init->streamlines->_lengths.size(), fixture.nb_streamlines); } TEST(TrxFileMemmap, deepcopy) { - trxmmap::TrxFile *trx = trxmmap::load_from_zip("data/small.trx"); + const auto &fixture = get_fixture(); + trxmmap::TrxFile *trx = trxmmap::load_from_zip(fixture.path); trxmmap::TrxFile *copy = trx->deepcopy(); EXPECT_EQ(trx->header, copy->header); @@ -307,13 +451,15 @@ TEST(TrxFileMemmap, deepcopy) TEST(TrxFileMemmap, resize) { - trxmmap::TrxFile *trx = trxmmap::load_from_zip("data/small.trx"); + const auto &fixture = get_fixture(); + trxmmap::TrxFile *trx = trxmmap::load_from_zip(fixture.path); trx->resize(); trx->resize(10); } TEST(TrxFileMemmap, save) { - trxmmap::TrxFile *trx = trxmmap::load_from_zip("data/small.trx"); + const auto &fixture = get_fixture(); + trxmmap::TrxFile *trx = trxmmap::load_from_zip(fixture.path); trxmmap::save(*trx, (std::string) "testsave"); trxmmap::save(*trx, (std::string) "testsave.trx"); From ccb65aec27b7be370606db8d32b3192003e1c976 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:28:51 -0500 Subject: [PATCH 02/19] Add github actions workflow --- .github/workflows/trx-cpp-tests.yml | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/trx-cpp-tests.yml diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml new file mode 100644 index 0000000..bf349e2 --- /dev/null +++ b/.github/workflows/trx-cpp-tests.yml @@ -0,0 +1,56 @@ +name: trx-cpp tests + +on: + push: + paths: + - "trx-cpp/**" + - "ITKSuperBuild/**" + - ".github/workflows/trx-cpp-tests.yml" + pull_request: + paths: + - "trx-cpp/**" + - "ITKSuperBuild/**" + - ".github/workflows/trx-cpp-tests.yml" + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake \ + g++ \ + libzip-dev \ + libeigen3-dev \ + nlohmann-json3-dev \ + libspdlog-dev + + - name: Fetch mio + run: | + git clone --depth 1 https://github.com/mandreyel/mio.git deps/mio + + - name: Build and install GoogleTest + run: | + git clone --depth 1 https://github.com/google/googletest.git deps/googletest + cmake -S deps/googletest -B deps/googletest/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=${GITHUB_WORKSPACE}/deps/googletest/install + cmake --build deps/googletest/build --target install + + - name: Configure trx-cpp tests + run: | + cmake -S trx-cpp/tests -B trx-cpp/tests/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio \ + -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/googletest/install + + - name: Build trx-cpp tests + run: cmake --build trx-cpp/tests/build + + - name: Run trx-cpp tests + run: ctest --test-dir trx-cpp/tests/build --output-on-failure From 60a46ec9b8fda995370ae88003060533d1aab347 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:32:31 -0500 Subject: [PATCH 03/19] oops --- .github/workflows/trx-cpp-tests.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index bf349e2..f796fb5 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -3,14 +3,10 @@ name: trx-cpp tests on: push: paths: - - "trx-cpp/**" - - "ITKSuperBuild/**" - - ".github/workflows/trx-cpp-tests.yml" + - "**" pull_request: paths: - - "trx-cpp/**" - - "ITKSuperBuild/**" - - ".github/workflows/trx-cpp-tests.yml" + - "**" jobs: build-and-test: @@ -44,13 +40,13 @@ jobs: - name: Configure trx-cpp tests run: | - cmake -S trx-cpp/tests -B trx-cpp/tests/build \ + cmake -S tests -B tests/build \ -DCMAKE_BUILD_TYPE=Release \ -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio \ -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/googletest/install - name: Build trx-cpp tests - run: cmake --build trx-cpp/tests/build + run: cmake --build tests/build - name: Run trx-cpp tests - run: ctest --test-dir trx-cpp/tests/build --output-on-failure + run: ctest --test-dir tests/build --output-on-failure From ae25b58afd19441714167ae8c9c5170d287c433d Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:34:51 -0500 Subject: [PATCH 04/19] install zcmp --- .github/workflows/trx-cpp-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index f796fb5..40abcfb 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -22,6 +22,7 @@ jobs: cmake \ g++ \ libzip-dev \ + zipcmp \ libeigen3-dev \ nlohmann-json3-dev \ libspdlog-dev From b5e1bee595784e00b141d0372fb63e10c405993c Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:37:12 -0500 Subject: [PATCH 05/19] more deps --- .github/workflows/trx-cpp-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 40abcfb..03be9ae 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -22,6 +22,7 @@ jobs: cmake \ g++ \ libzip-dev \ + libzip-tools \ zipcmp \ libeigen3-dev \ nlohmann-json3-dev \ From 50e122093bc703f665fec8c00c45fba448960f99 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:38:58 -0500 Subject: [PATCH 06/19] revert --- .github/workflows/trx-cpp-tests.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 03be9ae..4687e08 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -22,12 +22,19 @@ jobs: cmake \ g++ \ libzip-dev \ - libzip-tools \ - zipcmp \ libeigen3-dev \ nlohmann-json3-dev \ libspdlog-dev + - name: Ensure libzip tools are present + run: | + if ! command -v zipmerge >/dev/null 2>&1; then + sudo ln -s /bin/true /usr/bin/zipmerge + fi + if ! command -v zipcmp >/dev/null 2>&1; then + sudo ln -s /bin/true /usr/bin/zipcmp + fi + - name: Fetch mio run: | git clone --depth 1 https://github.com/mandreyel/mio.git deps/mio From 9970b489c8caa520c71018bc020de71acd7f70cd Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:40:40 -0500 Subject: [PATCH 07/19] continue --- .github/workflows/trx-cpp-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 4687e08..47bf655 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -34,6 +34,9 @@ jobs: if ! command -v zipcmp >/dev/null 2>&1; then sudo ln -s /bin/true /usr/bin/zipcmp fi + if ! command -v ziptool >/dev/null 2>&1; then + sudo ln -s /bin/true /usr/bin/ziptool + fi - name: Fetch mio run: | From 005aabc0fd7fa9ba6415c565281faeca35756cf7 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:42:23 -0500 Subject: [PATCH 08/19] correct include path --- .github/workflows/trx-cpp-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 47bf655..8553968 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -54,7 +54,7 @@ jobs: run: | cmake -S tests -B tests/build \ -DCMAKE_BUILD_TYPE=Release \ - -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio \ + -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio/single_include \ -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/googletest/install - name: Build trx-cpp tests From bcccc1e08c167671730a2ba685d739d3105291e6 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:43:32 -0500 Subject: [PATCH 09/19] actual correct include path --- .github/workflows/trx-cpp-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 8553968..e9e33f6 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -54,7 +54,7 @@ jobs: run: | cmake -S tests -B tests/build \ -DCMAKE_BUILD_TYPE=Release \ - -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio/single_include \ + -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio/include \ -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/googletest/install - name: Build trx-cpp tests From b58c687ac8c7d4ed5d94d9e7941c4375cfcbbfa2 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:46:28 -0500 Subject: [PATCH 10/19] fix for gh --- src/trx.tpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/trx.tpp b/src/trx.tpp index 8b6426c..91fb138 100644 --- a/src/trx.tpp +++ b/src/trx.tpp @@ -282,6 +282,7 @@ TrxFile
*_initialize_empty_trx(int nb_streamlines, int nb_vertices, const Tr new (&(trx->streamlines->_offsets)) Map>(reinterpret_cast(trx->streamlines->mmap_off.data()), std::get<0>(shape_off), std::get<1>(shape_off)); trx->streamlines->_lengths.resize(nb_streamlines); + trx->streamlines->_lengths.setZero(); if (init_as != NULL) { From 33d0fd45d919b2b5d2dfdf9b74a97157603c780d Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 16:48:45 -0500 Subject: [PATCH 11/19] address deprecation warnings --- src/trx.tpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/trx.tpp b/src/trx.tpp index 91fb138..1b49a5c 100644 --- a/src/trx.tpp +++ b/src/trx.tpp @@ -809,20 +809,20 @@ std::tuple TrxFile
::_copy_fixed_arrays_from(TrxFile
*trx, int if (curr_pts_len == 0) return std::make_tuple(strs_start, pts_start); - this->streamlines->_data(seq(pts_start, pts_end - 1), Eigen::placeholders::all) = trx->streamlines->_data(seq(0, curr_pts_len - 1), Eigen::placeholders::all); - this->streamlines->_offsets(seq(strs_start, strs_end - 1), Eigen::placeholders::all) = (trx->streamlines->_offsets(seq(0, curr_strs_len - 1), Eigen::placeholders::all).array() + pts_start).matrix(); - this->streamlines->_lengths(seq(strs_start, strs_end - 1), Eigen::placeholders::all) = trx->streamlines->_lengths(seq(0, curr_strs_len - 1), Eigen::placeholders::all); + this->streamlines->_data(seq(pts_start, pts_end - 1), Eigen::all) = trx->streamlines->_data(seq(0, curr_pts_len - 1), Eigen::all); + this->streamlines->_offsets(seq(strs_start, strs_end - 1), Eigen::all) = (trx->streamlines->_offsets(seq(0, curr_strs_len - 1), Eigen::all).array() + pts_start).matrix(); + this->streamlines->_lengths(seq(strs_start, strs_end - 1), Eigen::all) = trx->streamlines->_lengths(seq(0, curr_strs_len - 1), Eigen::all); for (auto const &x : this->data_per_vertex) { - this->data_per_vertex[x.first]->_data(seq(pts_start, pts_end - 1), Eigen::placeholders::all) = trx->data_per_vertex[x.first]->_data(seq(0, curr_pts_len - 1), Eigen::placeholders::all); + this->data_per_vertex[x.first]->_data(seq(pts_start, pts_end - 1), Eigen::all) = trx->data_per_vertex[x.first]->_data(seq(0, curr_pts_len - 1), Eigen::all); new (&(this->data_per_vertex[x.first]->_offsets)) Map>(trx->data_per_vertex[x.first]->_offsets.data(), trx->data_per_vertex[x.first]->_offsets.rows(), trx->data_per_vertex[x.first]->_offsets.cols()); this->data_per_vertex[x.first]->_lengths = trx->data_per_vertex[x.first]->_lengths; } for (auto const &x : this->data_per_streamline) { - this->data_per_streamline[x.first]->_matrix(seq(strs_start, strs_end - 1), Eigen::placeholders::all) = trx->data_per_streamline[x.first]->_matrix(seq(0, curr_strs_len - 1), Eigen::placeholders::all); + this->data_per_streamline[x.first]->_matrix(seq(strs_start, strs_end - 1), Eigen::all) = trx->data_per_streamline[x.first]->_matrix(seq(0, curr_strs_len - 1), Eigen::all); } return std::make_tuple(strs_end, pts_end); @@ -860,7 +860,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) if (nb_vertices == -1) { - ptrs_end = this->streamlines->_lengths(Eigen::placeholders::all, 0).sum(); + ptrs_end = this->streamlines->_lengths(Eigen::all, 0).sum(); nb_vertices = ptrs_end; } else if (nb_vertices < ptrs_end) @@ -883,7 +883,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) TrxFile
*trx = _initialize_empty_trx(nb_streamlines, nb_vertices, this); spdlog::info("Resizing streamlines from size {} to {}", this->streamlines->_lengths.size(), nb_streamlines); - spdlog::info("Resizing vertices from size {} to {}", this->streamlines->_data(Eigen::placeholders::all, 0).size(), nb_vertices); + spdlog::info("Resizing vertices from size {} to {}", this->streamlines->_data(Eigen::all, 0).size(), nb_vertices); if (nb_streamlines < this->header["NB_STREAMLINES"]) trx->_copy_fixed_arrays_from(this, -1, -1, nb_streamlines); From 45011ff5ad1fb8ce4f4e9e896066133d2d031f94 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 17:02:30 -0500 Subject: [PATCH 12/19] better handle the temp dir --- src/trx.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- src/trx.h | 1 + 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/trx.cpp b/src/trx.cpp index 39fda9c..8d35817 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #define SYSERROR() errno //#define ZIP_DD_SIG 0x08074b50 @@ -464,19 +465,57 @@ namespace trxmmap return rmdir(d); } - std::string extract_zip_to_directory(zip_t *zfolder) + std::string make_temp_dir(const std::string &prefix) { - if (zfolder == nullptr) + const char *candidates[] = {std::getenv("TMPDIR"), std::getenv("TEMP"), std::getenv("TMP")}; + std::string base_dir; + for (const char *candidate : candidates) { - throw std::invalid_argument("Zip archive pointer is null"); + if (candidate == nullptr || std::string(candidate).empty()) + { + continue; + } + std::filesystem::path path(candidate); + std::error_code ec; + if (std::filesystem::exists(path, ec) && std::filesystem::is_directory(path, ec)) + { + base_dir = path.string(); + break; + } + } + if (base_dir.empty()) + { + std::error_code ec; + auto sys_tmp = std::filesystem::temp_directory_path(ec); + if (!ec) + { + base_dir = sys_tmp.string(); + } + } + if (base_dir.empty()) + { + base_dir = "/tmp"; } - char t[] = "/tmp/trx_zip_XXXXXX"; - char *dirname = mkdtemp(t); + + std::filesystem::path tmpl = std::filesystem::path(base_dir) / (prefix + "_XXXXXX"); + std::string tmpl_str = tmpl.string(); + std::vector buf(tmpl_str.begin(), tmpl_str.end()); + buf.push_back('\0'); + char *dirname = mkdtemp(buf.data()); if (dirname == nullptr) { - throw std::runtime_error("Failed to create temporary directory for zip extraction"); + throw std::runtime_error("Failed to create temporary directory"); + } + return std::string(dirname); + } + + std::string extract_zip_to_directory(zip_t *zfolder) + { + if (zfolder == nullptr) + { + throw std::invalid_argument("Zip archive pointer is null"); } - std::string root_dir(dirname); + std::string root_dir = make_temp_dir("trx_zip"); zip_int64_t num_entries = zip_get_num_entries(zfolder, ZIP_FL_UNCHANGED); for (zip_int64_t i = 0; i < num_entries; ++i) diff --git a/src/trx.h b/src/trx.h index 56a09c6..ca7b294 100644 --- a/src/trx.h +++ b/src/trx.h @@ -292,6 +292,7 @@ namespace trxmmap void copy_dir(const char *src, const char *dst); void copy_file(const char *src, const char *dst); int rm_dir(const char *d); + std::string make_temp_dir(const std::string &prefix); std::string extract_zip_to_directory(zip_t *zfolder); std::string rm_root(std::string root, const std::string path); From eb3b75694a90280251a12ef689e7bb710f8e3662 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 17:09:43 -0500 Subject: [PATCH 13/19] try to figure out zip tools --- .github/workflows/trx-cpp-tests.yml | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index e9e33f6..d6e60c8 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -21,27 +21,25 @@ jobs: sudo apt-get install -y \ cmake \ g++ \ - libzip-dev \ + zlib1g-dev \ libeigen3-dev \ nlohmann-json3-dev \ libspdlog-dev - - name: Ensure libzip tools are present - run: | - if ! command -v zipmerge >/dev/null 2>&1; then - sudo ln -s /bin/true /usr/bin/zipmerge - fi - if ! command -v zipcmp >/dev/null 2>&1; then - sudo ln -s /bin/true /usr/bin/zipcmp - fi - if ! command -v ziptool >/dev/null 2>&1; then - sudo ln -s /bin/true /usr/bin/ziptool - fi - - name: Fetch mio run: | git clone --depth 1 https://github.com/mandreyel/mio.git deps/mio + - name: Build and install libzip (with tools) + run: | + git clone --depth 1 https://github.com/nih-at/libzip.git deps/libzip + cmake -S deps/libzip -B deps/libzip/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TOOLS=ON \ + -DBUILD_SHARED_LIBS=ON \ + -DCMAKE_INSTALL_PREFIX=${GITHUB_WORKSPACE}/deps/libzip/install + cmake --build deps/libzip/build --target install + - name: Build and install GoogleTest run: | git clone --depth 1 https://github.com/google/googletest.git deps/googletest @@ -55,7 +53,7 @@ jobs: cmake -S tests -B tests/build \ -DCMAKE_BUILD_TYPE=Release \ -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio/include \ - -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/googletest/install + -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/libzip/install;${GITHUB_WORKSPACE}/deps/googletest/install - name: Build trx-cpp tests run: cmake --build tests/build From 697ac26fe9e7627155a329af8165d0f94f115d60 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sat, 24 Jan 2026 17:19:43 -0500 Subject: [PATCH 14/19] google test --- .github/workflows/trx-cpp-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index d6e60c8..bb8bb69 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -53,7 +53,8 @@ jobs: cmake -S tests -B tests/build \ -DCMAKE_BUILD_TYPE=Release \ -DMIO_INCLUDE_DIR=${GITHUB_WORKSPACE}/deps/mio/include \ - -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/libzip/install;${GITHUB_WORKSPACE}/deps/googletest/install + -DGTest_DIR=${GITHUB_WORKSPACE}/deps/googletest/install/lib/cmake/GTest \ + -DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/deps/libzip/install - name: Build trx-cpp tests run: cmake --build tests/build From 521fe3731bf59b76a14bd13d0651f9f283a2248f Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sun, 25 Jan 2026 12:33:07 -0500 Subject: [PATCH 15/19] respond to review comments --- src/trx.cpp | 139 +++++++++++++++++++++++++++------------- src/trx.h | 3 + src/trx.tpp | 37 +++++++++-- tests/CMakeLists.txt | 4 +- tests/test_trx_mmap.cpp | 106 ++++++++++++++++++++++-------- 5 files changed, 211 insertions(+), 78 deletions(-) diff --git a/src/trx.cpp b/src/trx.cpp index 8d35817..2e109bf 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -106,6 +106,21 @@ namespace trxmmap } return ext; } + +bool _is_path_within(const std::filesystem::path &child, const std::filesystem::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; +} // TODO: check if there's a better way int _sizeof_dtype(std::string dtype) { @@ -296,10 +311,10 @@ namespace trxmmap // if file does not exist, create and allocate it struct stat buffer; - if (stat(filename.c_str(), &buffer) != 0) - { - allocate_file(filename, filesize); - } + if (stat(filename.c_str(), &buffer) != 0) + { + allocate_file(filename, filesize); + } if (filesize == 0) { @@ -492,10 +507,10 @@ namespace trxmmap base_dir = sys_tmp.string(); } } - if (base_dir.empty()) - { - base_dir = "/tmp"; - } + if (base_dir.empty()) + { + base_dir = "/tmp"; + } std::filesystem::path tmpl = std::filesystem::path(base_dir) / (prefix + "_XXXXXX"); std::string tmpl_str = tmpl.string(); @@ -515,60 +530,96 @@ namespace trxmmap { throw std::invalid_argument("Zip archive pointer is null"); } - std::string root_dir = make_temp_dir("trx_zip"); + std::string root_dir = make_temp_dir("trx_zip"); + std::filesystem::path normalized_root = std::filesystem::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) + zip_int64_t num_entries = zip_get_num_entries(zfolder, ZIP_FL_UNCHANGED); + for (zip_int64_t i = 0; i < num_entries; ++i) + { + const char *entry_name = zip_get_name(zfolder, i, ZIP_FL_UNCHANGED); + if (entry_name == nullptr) { - const char *entry_name = zip_get_name(zfolder, i, ZIP_FL_UNCHANGED); - if (entry_name == nullptr) - { - continue; - } - std::string entry(entry_name); - std::filesystem::path out_path = std::filesystem::path(root_dir) / entry; + continue; + } + std::string entry(entry_name); - if (!entry.empty() && entry.back() == '/') - { - std::error_code ec; - std::filesystem::create_directories(out_path, ec); - if (ec) - { - throw std::runtime_error("Failed to create directory: " + out_path.string()); - } - continue; - } + std::filesystem::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(); + + if (!_is_path_within(normalized_out, normalized_root)) + { + throw std::runtime_error("Zip entry escapes temporary directory: " + entry); + } + + if (!entry.empty() && entry.back() == '/') + { std::error_code ec; - std::filesystem::create_directories(out_path.parent_path(), ec); + std::filesystem::create_directories(normalized_out, ec); if (ec) { - throw std::runtime_error("Failed to create parent directory: " + out_path.parent_path().string()); + throw std::runtime_error("Failed to create directory: " + normalized_out.string()); } + continue; + } - zip_file_t *zf = zip_fopen_index(zfolder, i, ZIP_FL_UNCHANGED); - if (zf == nullptr) - { - throw std::runtime_error("Failed to open zip entry: " + entry); - } + std::error_code ec; + std::filesystem::create_directories(normalized_out.parent_path(), ec); + if (ec) + { + throw std::runtime_error("Failed to create parent directory: " + normalized_out.parent_path().string()); + } + + zip_file_t *zf = zip_fopen_index(zfolder, i, ZIP_FL_UNCHANGED); + if (zf == nullptr) + { + throw std::runtime_error("Failed to open zip entry: " + entry); + } + + std::ofstream out(normalized_out, std::ios::binary); + if (!out.is_open()) + { + zip_fclose(zf); + throw std::runtime_error("Failed to open output file: " + normalized_out.string()); + } - std::ofstream out(out_path, std::ios::binary); - if (!out.is_open()) + char buffer[4096]; + zip_int64_t nbytes = 0; + while ((nbytes = zip_fread(zf, buffer, sizeof(buffer))) > 0) + { + out.write(buffer, nbytes); + if (!out) { + out.close(); zip_fclose(zf); - throw std::runtime_error("Failed to open output file: " + out_path.string()); + throw std::runtime_error("Failed to write to output file: " + normalized_out.string()); } + } + if (nbytes < 0) + { + out.close(); + zip_fclose(zf); + throw std::runtime_error("Failed to read data from zip entry: " + entry); + } - char buffer[4096]; - zip_int64_t nbytes = 0; - while ((nbytes = zip_fread(zf, buffer, sizeof(buffer))) > 0) - { - out.write(buffer, nbytes); - } + out.flush(); + if (!out) + { out.close(); zip_fclose(zf); + throw std::runtime_error("Failed to flush output file: " + normalized_out.string()); } + out.close(); + zip_fclose(zf); + } + return root_dir; } diff --git a/src/trx.h b/src/trx.h index ca7b294..3461bf3 100644 --- a/src/trx.h +++ b/src/trx.h @@ -69,10 +69,12 @@ namespace trxmmap std::map *>> data_per_group; std::string _uncompressed_folder_handle; bool _copy_safe; + bool _owns_uncompressed_folder = false; // Member Functions() // TrxFile(int nb_vertices = 0, int nb_streamlines = 0); TrxFile(int nb_vertices = 0, int nb_streamlines = 0, const TrxFile
*init_as = NULL, std::string reference = ""); + ~TrxFile(); /** * @brief After reading the structure of a zip/folder, create a TrxFile @@ -106,6 +108,7 @@ namespace trxmmap * */ void close(); + void _cleanup_temporary_directory(); private: /** diff --git a/src/trx.tpp b/src/trx.tpp index 1b49a5c..c20320e 100644 --- a/src/trx.tpp +++ b/src/trx.tpp @@ -197,7 +197,11 @@ TrxFile
::TrxFile(int nb_vertices, int nb_streamlines, const TrxFile
*ini this->data_per_vertex = trx->data_per_vertex; this->data_per_group = trx->data_per_group; this->_uncompressed_folder_handle = trx->_uncompressed_folder_handle; + this->_owns_uncompressed_folder = trx->_owns_uncompressed_folder; this->_copy_safe = trx->_copy_safe; + trx->_owns_uncompressed_folder = false; + trx->_uncompressed_folder_handle.clear(); + delete trx; } else { @@ -385,6 +389,7 @@ TrxFile
*_initialize_empty_trx(int nb_streamlines, int nb_vertices, const Tr } trx->_uncompressed_folder_handle = tmp_dir; + trx->_owns_uncompressed_folder = true; return trx; } @@ -753,6 +758,7 @@ TrxFile
*TrxFile
::deepcopy() TrxFile
*copy_trx = load_from_directory
(tmp_dir); copy_trx->_uncompressed_folder_handle = tmp_dir; + copy_trx->_owns_uncompressed_folder = true; return copy_trx; } @@ -831,15 +837,31 @@ std::tuple TrxFile
::_copy_fixed_arrays_from(TrxFile
*trx, int template void TrxFile
::close() { - if (this->_uncompressed_folder_handle != "") - { - this->_uncompressed_folder_handle = ""; - } - + this->_cleanup_temporary_directory(); *this = TrxFile
(); // probably dangerous to do spdlog::debug("Deleted memmaps and initialized empty TrxFile."); } +template +TrxFile
::~TrxFile() +{ + this->_cleanup_temporary_directory(); +} + +template +void TrxFile
::_cleanup_temporary_directory() +{ + if (this->_owns_uncompressed_folder && !this->_uncompressed_folder_handle.empty()) + { + if (rm_dir(this->_uncompressed_folder_handle.c_str()) != 0) + { + spdlog::warn("Could not remove temporary folder {}", this->_uncompressed_folder_handle); + } + this->_uncompressed_folder_handle.clear(); + this->_owns_uncompressed_folder = false; + } +} + template void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) { @@ -1021,7 +1043,10 @@ TrxFile
*load_from_zip(std::string filename) std::string temp_dir = extract_zip_to_directory(zf); zip_close(zf); - return load_from_directory
(temp_dir); + TrxFile
*trx = load_from_directory
(temp_dir); + trx->_uncompressed_folder_handle = temp_dir; + trx->_owns_uncompressed_folder = true; + return trx; } template diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3390e30..70789e9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,8 +44,8 @@ if (TRX_ENABLE_TESTS AND GTest_FOUND) target_include_directories(test_mmap PRIVATE ${MIO_INCLUDE_DIR}) -set(CPACK_PROJECT_NAME ${PROJECT_NAME}) -set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) + set(CPACK_PROJECT_NAME ${PROJECT_NAME}) + set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) include(GoogleTest) gtest_discover_tests(test_mmap) diff --git a/tests/test_trx_mmap.cpp b/tests/test_trx_mmap.cpp index 49ae46c..9d7c425 100644 --- a/tests/test_trx_mmap.cpp +++ b/tests/test_trx_mmap.cpp @@ -3,6 +3,10 @@ #include #include #include +#include +#include +#include +#include using namespace Eigen; using namespace trxmmap; @@ -11,31 +15,72 @@ namespace { struct TestTrxFixture { + std::filesystem::path root_dir; std::string path; std::string dir_path; json expected_header; int nb_vertices; int nb_streamlines; + + ~TestTrxFixture() + { + std::error_code ec; + if (!root_dir.empty()) + { + std::filesystem::remove_all(root_dir, ec); + if (ec) + { + std::cerr << "Failed to clean up test directory " << root_dir.string() + << ": " << ec.message() << std::endl; + } + root_dir.clear(); + } + } }; - TestTrxFixture create_fixture() + std::filesystem::path make_temp_test_dir(const std::string &prefix) { - char t[] = "/tmp/trx_test_XXXXXX"; - char *dirname = mkdtemp(t); - if (dirname == nullptr) + std::error_code ec; + auto base = std::filesystem::temp_directory_path(ec); + if (ec) { - throw std::runtime_error("Failed to create temporary directory"); + throw std::runtime_error("Failed to get temp directory: " + ec.message()); } + static std::mt19937_64 rng(std::random_device{}()); + std::uniform_int_distribution dist; + + for (int attempt = 0; attempt < 100; ++attempt) + { + std::filesystem::path candidate = base / (prefix + "_" + std::to_string(dist(rng))); + std::error_code dir_ec; + if (std::filesystem::create_directory(candidate, dir_ec)) + { + return candidate; + } + if (dir_ec && dir_ec != std::errc::file_exists) + { + throw std::runtime_error("Failed to create temporary directory: " + dir_ec.message()); + } + } + throw std::runtime_error("Unable to create unique temporary directory"); + } + + TestTrxFixture create_fixture() + { + TestTrxFixture fixture; - std::string root_dir(dirname); - std::string trx_dir = root_dir + "/trx_data"; - if (mkdir(trx_dir.c_str(), S_IRWXU) != 0) + std::filesystem::path root_dir = make_temp_test_dir("trx_test"); + std::filesystem::path trx_dir = root_dir / "trx_data"; + std::error_code ec; + if (!std::filesystem::create_directory(trx_dir, ec) && ec) { - throw std::runtime_error("Failed to create trx data directory"); + throw std::runtime_error("Failed to create trx data directory: " + ec.message()); } - fixture.path = root_dir + "/small.trx"; - fixture.dir_path = trx_dir; + + fixture.root_dir = root_dir; + fixture.path = (root_dir / "small.trx").string(); + fixture.dir_path = trx_dir.string(); fixture.nb_vertices = 12; fixture.nb_streamlines = 4; @@ -48,7 +93,7 @@ namespace {0.0, 0.0, 0.0, 1.0}}; // Write header.json - std::string header_path = trx_dir + "/header.json"; + std::filesystem::path header_path = trx_dir / "header.json"; std::ofstream header_out(header_path); if (!header_out.is_open()) { @@ -60,7 +105,7 @@ namespace // Write positions (float16) Matrix positions(fixture.nb_vertices, 3); positions.setZero(); - std::string positions_path = trx_dir + "/positions.3.float16"; + std::filesystem::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) @@ -79,7 +124,7 @@ namespace { offsets(i, 0) = static_cast(i * (fixture.nb_vertices / fixture.nb_streamlines)); } - std::string offsets_path = trx_dir + "/offsets.uint64"; + std::filesystem::path offsets_path = trx_dir / "offsets.uint64"; trxmmap::write_binary(offsets_path.c_str(), offsets); if (stat(offsets_path.c_str(), &sb) != 0) { @@ -98,7 +143,7 @@ namespace { throw std::runtime_error("Failed to create trx zip file"); } - trxmmap::zip_from_folder(zf, trx_dir, trx_dir, ZIP_CM_STORE); + trxmmap::zip_from_folder(zf, trx_dir.string(), trx_dir.string(), ZIP_CM_STORE); if (zip_close(zf) != 0) { throw std::runtime_error("Failed to close trx zip file"); @@ -303,12 +348,8 @@ TEST(TrxFileMemmap, __dichotomic_search) TEST(TrxFileMemmap, __create_memmap) { - char *dirname; - char t[] = "/tmp/trx_XXXXXX"; - dirname = mkdtemp(t); - - std::string path(dirname); - path += "/offsets.int16"; + std::filesystem::path dir = make_temp_test_dir("trx_memmap"); + std::filesystem::path path = dir / "offsets.int16"; std::tuple shape = std::make_tuple(3, 4); @@ -331,15 +372,15 @@ TEST(TrxFileMemmap, __create_memmap) Map> real_m(reinterpret_cast(filled_mmap.data()), std::get<0>(shape), std::get<1>(shape)); EXPECT_EQ(expected_m, real_m); + + std::error_code ec; + std::filesystem::remove_all(dir, ec); } TEST(TrxFileMemmap, __create_memmap_empty) { - char t[] = "/tmp/trx_XXXXXX"; - char *dirname = mkdtemp(t); - - std::string path(dirname); - path += "/empty.float32"; + std::filesystem::path dir = make_temp_test_dir("trx_memmap_empty"); + std::filesystem::path path = dir / "empty.float32"; std::tuple shape = std::make_tuple(0, 1); mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path, shape); @@ -348,6 +389,9 @@ TEST(TrxFileMemmap, __create_memmap_empty) ASSERT_EQ(stat(path.c_str(), &sb), 0); EXPECT_EQ(sb.st_size, 0); EXPECT_EQ(empty_mmap.size(), 0u); + + std::error_code ec; + std::filesystem::remove_all(dir, ec); } TEST(TrxFileMemmap, load_header) @@ -384,6 +428,7 @@ TEST(TrxFileMemmap, load_zip) const auto &fixture = get_fixture(); trxmmap::TrxFile *trx = trxmmap::load_from_zip(fixture.path); EXPECT_GT(trx->streamlines->_data.size(), 0); + delete trx; } TEST(TrxFileMemmap, load_directory) @@ -391,6 +436,7 @@ TEST(TrxFileMemmap, load_directory) const auto &fixture = get_fixture(); trxmmap::TrxFile *trx = trxmmap::load_from_directory(fixture.dir_path); EXPECT_GT(trx->streamlines->_data.size(), 0); + delete trx; } TEST(TrxFileMemmap, TrxFile) @@ -435,6 +481,9 @@ TEST(TrxFileMemmap, TrxFile) EXPECT_EQ(trx_init->streamlines->_data.size(), fixture.nb_vertices * 3); EXPECT_EQ(trx_init->streamlines->_offsets.size(), fixture.nb_streamlines); EXPECT_EQ(trx_init->streamlines->_lengths.size(), fixture.nb_streamlines); + delete trx; + delete root_init; + delete trx_init; } TEST(TrxFileMemmap, deepcopy) @@ -447,6 +496,8 @@ TEST(TrxFileMemmap, deepcopy) EXPECT_EQ(trx->streamlines->_data, trx->streamlines->_data); EXPECT_EQ(trx->streamlines->_offsets, trx->streamlines->_offsets); EXPECT_EQ(trx->streamlines->_lengths, trx->streamlines->_lengths); + delete trx; + delete copy; } TEST(TrxFileMemmap, resize) @@ -455,6 +506,7 @@ TEST(TrxFileMemmap, resize) trxmmap::TrxFile *trx = trxmmap::load_from_zip(fixture.path); trx->resize(); trx->resize(10); + delete trx; } TEST(TrxFileMemmap, save) { @@ -463,6 +515,8 @@ TEST(TrxFileMemmap, save) trxmmap::save(*trx, (std::string) "testsave"); trxmmap::save(*trx, (std::string) "testsave.trx"); + delete trx; + // trxmmap::TrxFile *saved = trxmmap::load_from_zip("testsave.trx"); // EXPECT_EQ(saved->data_per_vertex["color_x.float16"]->_data, trx->data_per_vertex["color_x.float16"]->_data); } From 4bf1c9dba6a37b3d3c2cd7e9c84fc26dc2102399 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sun, 25 Jan 2026 13:16:04 -0500 Subject: [PATCH 16/19] Address differences with #5 --- CMakeLists.txt | 54 +++++++++++++++++++++++----- include/trx/trx.h | 10 ++++++ src/trx.cpp | 63 +++++++++++++++++++++----------- src/trx.h | 5 ++- src/trx.tpp | 79 ++++++++++++++++++----------------------- tests/CMakeLists.txt | 54 ++++------------------------ tests/test_trx_mmap.cpp | 47 ++++++++++++------------ 7 files changed, 168 insertions(+), 144 deletions(-) create mode 100644 include/trx/trx.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f63483c..de45f69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,11 +4,24 @@ cmake_policy(SET CMP0079 NEW) project(trx VERSION 0.1.0) set (CMAKE_CXX_STANDARD 17) +option(TRX_USE_CONAN "Should Conan package manager be used?" ON) +option(TRX_BUILD_TESTS "Build trx tests" ON) + #set(CMAKE_BUILD_TYPE RelWithDebInfo) set(CMAKE_BUILD_TYPE Debug) find_package(libzip REQUIRED) -find_package (Eigen3 CONFIG REQUIRED) +find_package(Eigen3 CONFIG QUIET) +if (NOT Eigen3_FOUND) + find_package(Eigen3 REQUIRED) # try module mode +endif() +# Create an imported target if the package did not provide one (module mode) +if (NOT TARGET Eigen3::Eigen AND EXISTS "${EIGEN3_INCLUDE_DIR}") + add_library(Eigen3::Eigen INTERFACE IMPORTED) + set_target_properties(Eigen3::Eigen PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${EIGEN3_INCLUDE_DIR}" + ) +endif() find_package(nlohmann_json CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) find_path(MIO_INCLUDE_DIR mio/mmap.hpp PATH_SUFFIXES include) @@ -18,15 +31,40 @@ endif() add_library(trx src/trx.cpp src/trx.tpp src/trx.h) +if(TRX_USE_CONAN AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ConanSetup.cmake") + include(cmake/ConanSetup.cmake) +elseif(TRX_USE_CONAN) + message(STATUS "TRX_USE_CONAN enabled but cmake/ConanSetup.cmake not found; skipping Conan.") +endif() + TARGET_LINK_LIBRARIES(trx - PRIVATE - nlohmann_json::nlohmann_json - libzip::zip - Eigen3::Eigen - spdlog::spdlog - spdlog::spdlog_header_only + PUBLIC + nlohmann_json::nlohmann_json + libzip::zip + Eigen3::Eigen + spdlog::spdlog + spdlog::spdlog_header_only +) +target_include_directories(trx + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${MIO_INCLUDE_DIR} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ) -target_include_directories(trx PRIVATE ${MIO_INCLUDE_DIR}) + +if(TRX_BUILD_TESTS) + find_package(GTest CONFIG QUIET) + if(NOT GTest_FOUND) + find_package(GTest QUIET) + endif() + if(GTest_FOUND) + enable_testing() + add_subdirectory(tests) + else() + message(STATUS "GTest not found; skipping tests. Set GTest_DIR to a config path to enable.") + endif() +endif() set(CPACK_PROJECT_NAME ${PROJECT_NAME}) diff --git a/include/trx/trx.h b/include/trx/trx.h new file mode 100644 index 0000000..b8de775 --- /dev/null +++ b/include/trx/trx.h @@ -0,0 +1,10 @@ +#pragma once + +// Provide the public include path expected by consumers. +// This wrapper forwards to the actual header in src/ while +// also ensuring SPDLOG uses the external fmt if provided. +#ifndef SPDLOG_FMT_EXTERNAL +#define SPDLOG_FMT_EXTERNAL +#endif + +#include "../../src/trx.h" diff --git a/src/trx.cpp b/src/trx.cpp index 2e109bf..9390a14 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -298,7 +298,7 @@ bool _is_path_within(const std::filesystem::path &child, const std::filesystem:: } } - mio::shared_mmap_sink _create_memmap(std::string &filename, std::tuple &shape, std::string mode, std::string dtype, long long offset) +mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple &shape, std::string mode, std::string dtype, long long offset) { if (dtype.compare("bool") == 0) { @@ -323,7 +323,7 @@ bool _is_path_within(const std::filesystem::path &child, const std::filesystem:: // std::error_code error; - mio::shared_mmap_sink rw_mmap(filename, offset, filesize); + mio::shared_mmap_sink rw_mmap(filename, offset, filesize); return rw_mmap; } @@ -481,9 +481,31 @@ bool _is_path_within(const std::filesystem::path &child, const std::filesystem:: } std::string make_temp_dir(const std::string &prefix) + { + const char *env_tmp = std::getenv("TRX_TMPDIR"); + std::string base_dir; + + if (env_tmp != nullptr) + { + std::string val(env_tmp); + if (val == "use_working_dir") + { + base_dir = "."; + } + else + { + std::filesystem::path env_path(val); + std::error_code ec; + if (std::filesystem::exists(env_path, ec) && std::filesystem::is_directory(env_path, ec)) + { + base_dir = env_path.string(); + } + } + } + + if (base_dir.empty()) { const char *candidates[] = {std::getenv("TMPDIR"), std::getenv("TEMP"), std::getenv("TMP")}; - std::string base_dir; for (const char *candidate : candidates) { if (candidate == nullptr || std::string(candidate).empty()) @@ -498,30 +520,31 @@ bool _is_path_within(const std::filesystem::path &child, const std::filesystem:: break; } } - if (base_dir.empty()) + } + if (base_dir.empty()) + { + std::error_code ec; + auto sys_tmp = std::filesystem::temp_directory_path(ec); + if (!ec) { - std::error_code ec; - auto sys_tmp = std::filesystem::temp_directory_path(ec); - if (!ec) - { - base_dir = sys_tmp.string(); - } + base_dir = sys_tmp.string(); } + } if (base_dir.empty()) { base_dir = "/tmp"; } - std::filesystem::path tmpl = std::filesystem::path(base_dir) / (prefix + "_XXXXXX"); - std::string tmpl_str = tmpl.string(); - std::vector buf(tmpl_str.begin(), tmpl_str.end()); - buf.push_back('\0'); - char *dirname = mkdtemp(buf.data()); - if (dirname == nullptr) - { - throw std::runtime_error("Failed to create temporary directory"); - } - return std::string(dirname); + std::filesystem::path tmpl = std::filesystem::path(base_dir) / (prefix + "_XXXXXX"); + std::string tmpl_str = tmpl.string(); + std::vector buf(tmpl_str.begin(), tmpl_str.end()); + buf.push_back('\0'); + char *dirname = mkdtemp(buf.data()); + if (dirname == nullptr) + { + throw std::runtime_error("Failed to create temporary directory"); + } + return std::string(dirname); } std::string extract_zip_to_directory(zip_t *zfolder) diff --git a/src/trx.h b/src/trx.h index 3461bf3..94c9585 100644 --- a/src/trx.h +++ b/src/trx.h @@ -20,6 +20,9 @@ #include #include +#ifndef SPDLOG_FMT_EXTERNAL +#define SPDLOG_FMT_EXTERNAL +#endif #include "spdlog/spdlog.h" using namespace Eigen; @@ -225,7 +228,7 @@ namespace trxmmap // TODO: ADD order?? // TODO: change tuple to vector to support ND arrays? // TODO: remove data type as that's done outside of this function - mio::shared_mmap_sink _create_memmap(std::string &filename, std::tuple &shape, std::string mode = "r", std::string dtype = "float32", long long offset = 0); + mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple &shape, std::string mode = "r", std::string dtype = "float32", long long offset = 0); template std::string _generate_filename_from_data(const MatrixBase
&arr, const std::string filename); diff --git a/src/trx.tpp b/src/trx.tpp index c20320e..730a8f0 100644 --- a/src/trx.tpp +++ b/src/trx.tpp @@ -75,31 +75,17 @@ Matrix _compute_lengths(const MatrixBase
&offsets, int { if (offsets.size() > 1) { - int last_elem_pos = _dichotomic_search(offsets); - Matrix lengths; - - if (last_elem_pos == offsets.size() - 1) - { - Matrix tmp(offsets.template cast()); - ediff1d(lengths, tmp, uint32_t(nb_vertices - offsets(offsets.size() - 1))); - } - else + const auto casted = offsets.template cast(); + const Eigen::Index len = offsets.size() - 1; + Matrix lengths(len); + for (Eigen::Index i = 0; i < len; ++i) { - Matrix tmp(offsets.template cast()); - tmp(last_elem_pos + 1) = uint32_t(nb_vertices); - ediff1d(lengths, tmp, 0); - lengths(last_elem_pos + 1) = uint32_t(0); + lengths(i) = static_cast(casted(i + 1) - casted(i)); } return lengths; } - if (offsets.size() == 1) - { - Matrix lengths(nb_vertices); - return lengths; - } - - Matrix lengths(0); - return lengths; + // If offsets are empty or only contain the sentinel, there are zero streamlines. + return Matrix(0); } template @@ -221,11 +207,7 @@ TrxFile
*_initialize_empty_trx(int nb_streamlines, int nb_vertices, const Tr { TrxFile
*trx = new TrxFile
(); - char *dirname; - char t[] = "/tmp/trx_XXXXXX"; - dirname = mkdtemp(t); - - std::string tmp_dir(dirname); + std::string tmp_dir = make_temp_dir("trx"); spdlog::info("Temporary folder for memmaps: {}", tmp_dir); @@ -280,7 +262,7 @@ TrxFile
*_initialize_empty_trx(int nb_streamlines, int nb_vertices, const Tr std::string offsets_filename(tmp_dir); offsets_filename += "/offsets." + offsets_dtype; - std::tuple shape_off = std::make_tuple(nb_streamlines, 1); + std::tuple shape_off = std::make_tuple(nb_streamlines + 1, 1); trx->streamlines->mmap_off = trxmmap::_create_memmap(offsets_filename, shape_off, "w+", offsets_dtype); new (&(trx->streamlines->_offsets)) Map>(reinterpret_cast(trx->streamlines->mmap_off.data()), std::get<0>(shape_off), std::get<1>(shape_off)); @@ -481,7 +463,7 @@ TrxFile
*TrxFile
::_create_trx_from_pointer(json header, std::mapheader["NB_STREAMLINES"]) || dim != 1) + if (size != int(trx->header["NB_STREAMLINES"]) + 1 || dim != 1) { throw std::invalid_argument("Wrong offsets size/dimensionality: size=" + std::to_string(size) + " nb_streamlines=" + @@ -489,14 +471,13 @@ TrxFile
*TrxFile
::_create_trx_from_pointer(json header, std::mapheader["NB_STREAMLINES"]); + std::tuple shape = std::make_tuple(nb_str + 1, 1); trx->streamlines->mmap_off = trxmmap::_create_memmap(filename, shape, "r+", ext, mem_adress); new (&(trx->streamlines->_offsets)) Map>(reinterpret_cast(trx->streamlines->mmap_off.data()), std::get<0>(shape), std::get<1>(shape)); - // TODO : adapt compute_lengths to accept a map - Matrix offsets; - offsets = trx->streamlines->_offsets; + Matrix offsets = trx->streamlines->_offsets; trx->streamlines->_lengths = _compute_lengths(offsets, int(trx->header["NB_VERTICES"])); } @@ -632,11 +613,7 @@ TrxFile
*TrxFile
::_create_trx_from_pointer(json header, std::map TrxFile
*TrxFile
::deepcopy() { - char *dirname; - char t[] = "/tmp/trx_XXXXXX"; - dirname = mkdtemp(t); - - std::string tmp_dir(dirname); + std::string tmp_dir = make_temp_dir("trx"); std::string header = tmp_dir + SEPARATOR + "header.json"; std::ofstream out_json(header); @@ -652,9 +629,14 @@ TrxFile
*TrxFile
::deepcopy() if (!this->_copy_safe) { - tmp_header["NB_STREAMLINES"] = to_dump->_offsets.size(); + tmp_header["NB_STREAMLINES"] = to_dump->_offsets.size() > 0 ? to_dump->_offsets.size() - 1 : 0; tmp_header["NB_VERTICES"] = to_dump->_data.size() / 3; } + // Ensure sentinel is correct before persisting + if (to_dump->_offsets.size() > 0) + { + to_dump->_offsets(to_dump->_offsets.size() - 1) = tmp_header["NB_VERTICES"]; + } if (out_json.is_open()) { out_json << std::setw(4) << tmp_header << std::endl; @@ -815,20 +797,27 @@ std::tuple TrxFile
::_copy_fixed_arrays_from(TrxFile
*trx, int if (curr_pts_len == 0) return std::make_tuple(strs_start, pts_start); - this->streamlines->_data(seq(pts_start, pts_end - 1), Eigen::all) = trx->streamlines->_data(seq(0, curr_pts_len - 1), Eigen::all); - this->streamlines->_offsets(seq(strs_start, strs_end - 1), Eigen::all) = (trx->streamlines->_offsets(seq(0, curr_strs_len - 1), Eigen::all).array() + pts_start).matrix(); - this->streamlines->_lengths(seq(strs_start, strs_end - 1), Eigen::all) = trx->streamlines->_lengths(seq(0, curr_strs_len - 1), Eigen::all); + this->streamlines->_data.block(pts_start, 0, curr_pts_len, this->streamlines->_data.cols()) = + trx->streamlines->_data.block(0, 0, curr_pts_len, trx->streamlines->_data.cols()); + + this->streamlines->_offsets.block(strs_start, 0, curr_strs_len + 1, 1) = + (trx->streamlines->_offsets.block(0, 0, curr_strs_len + 1, 1).array() + pts_start).matrix(); + + this->streamlines->_lengths.block(strs_start, 0, curr_strs_len, 1) = + trx->streamlines->_lengths.block(0, 0, curr_strs_len, 1); for (auto const &x : this->data_per_vertex) { - this->data_per_vertex[x.first]->_data(seq(pts_start, pts_end - 1), Eigen::all) = trx->data_per_vertex[x.first]->_data(seq(0, curr_pts_len - 1), Eigen::all); + this->data_per_vertex[x.first]->_data.block(pts_start, 0, curr_pts_len, this->data_per_vertex[x.first]->_data.cols()) = + trx->data_per_vertex[x.first]->_data.block(0, 0, curr_pts_len, trx->data_per_vertex[x.first]->_data.cols()); new (&(this->data_per_vertex[x.first]->_offsets)) Map>(trx->data_per_vertex[x.first]->_offsets.data(), trx->data_per_vertex[x.first]->_offsets.rows(), trx->data_per_vertex[x.first]->_offsets.cols()); this->data_per_vertex[x.first]->_lengths = trx->data_per_vertex[x.first]->_lengths; } for (auto const &x : this->data_per_streamline) { - this->data_per_streamline[x.first]->_matrix(seq(strs_start, strs_end - 1), Eigen::all) = trx->data_per_streamline[x.first]->_matrix(seq(0, curr_strs_len - 1), Eigen::all); + this->data_per_streamline[x.first]->_matrix.block(strs_start, 0, curr_strs_len, this->data_per_streamline[x.first]->_matrix.cols()) = + trx->data_per_streamline[x.first]->_matrix.block(0, 0, curr_strs_len, trx->data_per_streamline[x.first]->_matrix.cols()); } return std::make_tuple(strs_end, pts_end); @@ -882,7 +871,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) if (nb_vertices == -1) { - ptrs_end = this->streamlines->_lengths(Eigen::all, 0).sum(); + ptrs_end = this->streamlines->_lengths.sum(); nb_vertices = ptrs_end; } else if (nb_vertices < ptrs_end) @@ -905,7 +894,7 @@ void TrxFile
::resize(int nb_streamlines, int nb_vertices, bool delete_dpg) TrxFile
*trx = _initialize_empty_trx(nb_streamlines, nb_vertices, this); spdlog::info("Resizing streamlines from size {} to {}", this->streamlines->_lengths.size(), nb_streamlines); - spdlog::info("Resizing vertices from size {} to {}", this->streamlines->_data(Eigen::all, 0).size(), nb_vertices); + spdlog::info("Resizing vertices from size {} to {}", this->streamlines->_data.rows(), nb_vertices); if (nb_streamlines < this->header["NB_STREAMLINES"]) trx->_copy_fixed_arrays_from(this, -1, -1, nb_streamlines); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 70789e9..56fb1af 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,52 +1,12 @@ -cmake_minimum_required(VERSION 3.10) -cmake_policy(SET CMP0074 NEW) -cmake_policy(SET CMP0079 NEW) -project(trx) -set (CMAKE_CXX_STANDARD 17) +enable_testing() -set(PROJECT_BINARY_DIR ../../builds) -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/build/tests) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/build/tests) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - -set(CMAKE_BUILD_TYPE Debug) - -find_package(libzip REQUIRED) -find_package (Eigen3 CONFIG REQUIRED) -find_package(nlohmann_json CONFIG REQUIRED) -find_package(spdlog CONFIG REQUIRED) -option(TRX_ENABLE_TESTS "Build trx-cpp tests" ON) find_package(GTest CONFIG QUIET) -if (TRX_ENABLE_TESTS AND NOT GTest_FOUND) - message(FATAL_ERROR "GTest not found. Set GTest_DIR/CMAKE_PREFIX_PATH or pass -DTRX_ENABLE_TESTS=OFF to skip tests.") +if(NOT GTest_FOUND) + find_package(GTest REQUIRED) endif() -find_path(MIO_INCLUDE_DIR mio/mmap.hpp PATH_SUFFIXES include) -if (NOT MIO_INCLUDE_DIR) - message(FATAL_ERROR "mio headers not found. Set MIO_INCLUDE_DIR to the folder containing mio/mmap.hpp.") -endif() - -if (TRX_ENABLE_TESTS AND GTest_FOUND) - enable_testing() - include_directories(../src) - add_executable(test_mmap test_trx_mmap.cpp ../src/trx.h ../src/trx.tpp ../src/trx.cpp) +add_executable(test_mmap test_trx_mmap.cpp) +target_link_libraries(test_mmap PRIVATE trx GTest::gtest GTest::gtest_main) - - TARGET_LINK_LIBRARIES(test_mmap - nlohmann_json::nlohmann_json - Eigen3::Eigen - libzip::zip - GTest::gtest - GTest::gtest_main - spdlog::spdlog - spdlog::spdlog_header_only - ) - target_include_directories(test_mmap PRIVATE ${MIO_INCLUDE_DIR}) - - - set(CPACK_PROJECT_NAME ${PROJECT_NAME}) - set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) - include(CPack) - include(GoogleTest) - gtest_discover_tests(test_mmap) -endif() +include(GoogleTest) +gtest_discover_tests(test_mmap) diff --git a/tests/test_trx_mmap.cpp b/tests/test_trx_mmap.cpp index 9d7c425..30b13da 100644 --- a/tests/test_trx_mmap.cpp +++ b/tests/test_trx_mmap.cpp @@ -1,5 +1,5 @@ #include -#include "../src/trx.h" +#include #include #include #include @@ -118,19 +118,21 @@ namespace throw std::runtime_error("Positions file size mismatch"); } - // Write offsets (uint64) - Matrix offsets(fixture.nb_streamlines, 1); + // Write offsets (uint64) with sentinel (NB_STREAMLINES + 1) + Matrix offsets(fixture.nb_streamlines + 1, 1); for (int i = 0; i < fixture.nb_streamlines; ++i) { offsets(i, 0) = static_cast(i * (fixture.nb_vertices / fixture.nb_streamlines)); } + offsets(fixture.nb_streamlines, 0) = static_cast(fixture.nb_vertices); + std::filesystem::path offsets_path = trx_dir / "offsets.uint64"; trxmmap::write_binary(offsets_path.c_str(), offsets); if (stat(offsets_path.c_str(), &sb) != 0) { throw std::runtime_error("Failed to stat offsets file"); } - const size_t expected_offsets_bytes = fixture.nb_streamlines * sizeof(uint64_t); + const size_t expected_offsets_bytes = (fixture.nb_streamlines + 1) * sizeof(uint64_t); if (static_cast(sb.st_size) != expected_offsets_bytes) { throw std::runtime_error("Offsets file size mismatch"); @@ -270,34 +272,33 @@ TEST(TrxFileMemmap, __split_ext_with_dimensionality) TEST(TrxFileMemmap, __compute_lengths) { Matrix offsets{uint64_t(0), uint64_t(1), uint64_t(2), uint64_t(3), uint64_t(4)}; - Matrix lengths(trxmmap::_compute_lengths(offsets, 4)); - Matrix result{uint32_t(1), uint32_t(1), uint32_t(1), uint32_t(1), uint32_t(0)}; + Matrix lengths(trxmmap::_compute_lengths(offsets, 4)); + Matrix result{uint32_t(1), uint32_t(1), uint32_t(1), uint32_t(1)}; EXPECT_EQ(lengths, result); - Matrix offsets2{uint64_t(0), uint64_t(1), uint64_t(0), uint64_t(3), uint64_t(4)}; - Matrix lengths2(trxmmap::_compute_lengths(offsets2, 4)); - Matrix result2{uint32_t(1), uint32_t(3), uint32_t(0), uint32_t(1), uint32_t(0)}; + Matrix offsets2{uint64_t(0), uint64_t(1), uint64_t(1), uint64_t(3), uint64_t(4)}; + Matrix lengths2(trxmmap::_compute_lengths(offsets2, 4)); + Matrix result2{uint32_t(1), uint32_t(0), uint32_t(2), uint32_t(1)}; EXPECT_EQ(lengths2, result2); - Matrix offsets3{uint64_t(0), uint64_t(1), uint64_t(2), uint64_t(3)}; - Matrix lengths3(trxmmap::_compute_lengths(offsets3, 4)); - Matrix result3{uint32_t(1), uint32_t(1), uint32_t(1), uint32_t(1)}; + Matrix offsets3{uint64_t(0), uint64_t(1), uint64_t(2), uint64_t(4)}; + Matrix lengths3(trxmmap::_compute_lengths(offsets3, 4)); + Matrix result3{uint32_t(1), uint32_t(1), uint32_t(2)}; EXPECT_EQ(lengths3, result3); - Matrix offsets4(uint64_t(4)); + Matrix offsets4; + offsets4 << uint64_t(0), uint64_t(2); Matrix lengths4(trxmmap::_compute_lengths(offsets4, 2)); Matrix result4(uint32_t(2)); EXPECT_EQ(lengths4, result4); Matrix offsets5; - Matrix lengths5(trxmmap::_compute_lengths(offsets5, 2)); - Matrix result5(uint32_t(0)); - - EXPECT_EQ(lengths5, result5); + Matrix lengths5(trxmmap::_compute_lengths(offsets5, 2)); + EXPECT_EQ(lengths5.size(), 0); } TEST(TrxFileMemmap, __is_dtype_valid) @@ -349,12 +350,12 @@ TEST(TrxFileMemmap, __create_memmap) { std::filesystem::path dir = make_temp_test_dir("trx_memmap"); - std::filesystem::path path = dir / "offsets.int16"; + std::filesystem::path path = dir / "offsets.int16"; std::tuple shape = std::make_tuple(3, 4); // Test 1: create file and allocate space assert that correct data is filled - mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path, shape); + mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path.string(), shape); Map> expected_m(reinterpret_cast(empty_mmap.data())); Matrix zero_filled{{half(0), half(0), half(0), half(0)}, {half(0), half(0), half(0), half(0)}, @@ -368,7 +369,7 @@ TEST(TrxFileMemmap, __create_memmap) expected_m(i) = half(i); } - mio::shared_mmap_sink filled_mmap = trxmmap::_create_memmap(path, shape); + mio::shared_mmap_sink filled_mmap = trxmmap::_create_memmap(path.string(), shape); Map> real_m(reinterpret_cast(filled_mmap.data()), std::get<0>(shape), std::get<1>(shape)); EXPECT_EQ(expected_m, real_m); @@ -380,10 +381,10 @@ TEST(TrxFileMemmap, __create_memmap) TEST(TrxFileMemmap, __create_memmap_empty) { std::filesystem::path dir = make_temp_test_dir("trx_memmap_empty"); - std::filesystem::path path = dir / "empty.float32"; + std::filesystem::path path = dir / "empty.float32"; std::tuple shape = std::make_tuple(0, 1); - mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path, shape); + mio::shared_mmap_sink empty_mmap = trxmmap::_create_memmap(path.string(), shape); struct stat sb; ASSERT_EQ(stat(path.c_str(), &sb), 0); @@ -479,7 +480,7 @@ TEST(TrxFileMemmap, TrxFile) EXPECT_EQ(root_init->header, init_as); EXPECT_EQ(trx_init->streamlines->_data.size(), fixture.nb_vertices * 3); - EXPECT_EQ(trx_init->streamlines->_offsets.size(), fixture.nb_streamlines); + EXPECT_EQ(trx_init->streamlines->_offsets.size(), fixture.nb_streamlines + 1); EXPECT_EQ(trx_init->streamlines->_lengths.size(), fixture.nb_streamlines); delete trx; delete root_init; From 14bd2edbe12c3cb09cd3510d68ec8d490cd08346 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sun, 25 Jan 2026 13:21:08 -0500 Subject: [PATCH 17/19] gh test --- .github/workflows/trx-cpp-tests.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index bb8bb69..0217c37 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -48,16 +48,16 @@ jobs: -DCMAKE_INSTALL_PREFIX=${GITHUB_WORKSPACE}/deps/googletest/install cmake --build deps/googletest/build --target install - - name: Configure trx-cpp tests - run: | - cmake -S tests -B tests/build \ - -DCMAKE_BUILD_TYPE=Release \ - -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 - - - name: Build trx-cpp tests - run: cmake --build tests/build - - - name: Run trx-cpp tests - run: ctest --test-dir tests/build --output-on-failure + - name: Configure trx-cpp (root) + run: | + cmake -S . -B build \ + -DTRX_BUILD_TESTS=ON \ + -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 + + - name: Build + run: cmake --build build + + - name: Test + run: ctest --test-dir build --output-on-failure \ No newline at end of file From d6fbee451a30ebc9f90da17e9fae73443a530b16 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sun, 25 Jan 2026 13:21:15 -0500 Subject: [PATCH 18/19] gh test --- .github/workflows/trx-cpp-tests.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 0217c37..10e7e13 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -48,16 +48,16 @@ jobs: -DCMAKE_INSTALL_PREFIX=${GITHUB_WORKSPACE}/deps/googletest/install cmake --build deps/googletest/build --target install - - name: Configure trx-cpp (root) - run: | - cmake -S . -B build \ - -DTRX_BUILD_TESTS=ON \ - -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 - - - name: Build - run: cmake --build build - - - name: Test - run: ctest --test-dir build --output-on-failure \ No newline at end of file + - name: Configure trx-cpp + run: | + cmake -S . -B build \ + -DTRX_BUILD_TESTS=ON \ + -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 + + - name: Build + run: cmake --build build + + - name: Test + run: ctest --test-dir build --output-on-failure \ No newline at end of file From 40b9d94daf43e042818a4cfa1e245c84dd3cdb36 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Sun, 25 Jan 2026 21:06:27 -0500 Subject: [PATCH 19/19] add conan build and test --- .github/workflows/trx-cpp-tests.yml | 25 ++- .gitignore | 5 + CMakeLists.txt | 104 ++++++++-- cmake/trx-cppConfig.cmake.in | 13 ++ conanfile.py | 132 ++++++++++++ include/trx/trx.h | 309 +++++++++++++++++++++++++++- {src => include/trx}/trx.tpp | 0 src/trx.cpp | 2 +- src/trx.h | 309 ---------------------------- test_package/CMakeLists.txt | 10 + test_package/conanfile.py | 25 +++ test_package/src/example.cpp | 9 + 12 files changed, 612 insertions(+), 331 deletions(-) create mode 100644 cmake/trx-cppConfig.cmake.in create mode 100644 conanfile.py rename {src => include/trx}/trx.tpp (100%) delete mode 100644 src/trx.h create mode 100644 test_package/CMakeLists.txt create mode 100644 test_package/conanfile.py create mode 100644 test_package/src/example.cpp diff --git a/.github/workflows/trx-cpp-tests.yml b/.github/workflows/trx-cpp-tests.yml index 10e7e13..04de69b 100644 --- a/.github/workflows/trx-cpp-tests.yml +++ b/.github/workflows/trx-cpp-tests.yml @@ -60,4 +60,27 @@ jobs: run: cmake --build build - name: Test - run: ctest --test-dir build --output-on-failure \ No newline at end of file + run: ctest --test-dir build --output-on-failure + + conan-create: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build tooling + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build g++ + + - name: Install Conan + run: | + python3 -m pip install --upgrade pip + python3 -m pip install "conan>=2.0,<3.0" + + - name: Conan create (with tests) + 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 diff --git a/.gitignore b/.gitignore index 94a144d..c1d7cb4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ libtrx.a tests/data .vscode +test_package/build/ +test_package/CMakeUserPresets.json + +test_package/build +test_package/CMakeUserPresets.json \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index de45f69..9a6766b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,31 @@ cmake_minimum_required(VERSION 3.10) cmake_policy(SET CMP0074 NEW) cmake_policy(SET CMP0079 NEW) + project(trx VERSION 0.1.0) -set (CMAKE_CXX_STANDARD 17) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +set(CMAKE_CXX_STANDARD 17) +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_BUILD_TESTS "Build trx tests" ON) -#set(CMAKE_BUILD_TYPE RelWithDebInfo) -set(CMAKE_BUILD_TYPE Debug) - find_package(libzip REQUIRED) +set(TRX_LIBZIP_TARGET "") +if(TARGET libzip::libzip) + set(TRX_LIBZIP_TARGET libzip::libzip) +elseif(TARGET libzip::zip) + set(TRX_LIBZIP_TARGET libzip::zip) +elseif(TARGET zip::zip) + set(TRX_LIBZIP_TARGET zip::zip) +else() + message(FATAL_ERROR "No suitable libzip target (expected libzip::libzip or zip::zip)") +endif() find_package(Eigen3 CONFIG QUIET) if (NOT Eigen3_FOUND) find_package(Eigen3 REQUIRED) # try module mode @@ -24,12 +39,18 @@ if (NOT TARGET Eigen3::Eigen AND EXISTS "${EIGEN3_INCLUDE_DIR}") endif() find_package(nlohmann_json CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) -find_path(MIO_INCLUDE_DIR mio/mmap.hpp PATH_SUFFIXES include) -if (NOT MIO_INCLUDE_DIR) - message(FATAL_ERROR "mio headers not found. Set MIO_INCLUDE_DIR to the folder containing mio/mmap.hpp.") +find_package(mio CONFIG QUIET) +if(TARGET mio::mio) + set(TRX_HAVE_MIO_TARGET ON) +else() + find_path(MIO_INCLUDE_DIR mio/mmap.hpp PATH_SUFFIXES include) + if (NOT MIO_INCLUDE_DIR) + message(FATAL_ERROR "mio headers not found. Set MIO_INCLUDE_DIR to the folder containing mio/mmap.hpp.") + endif() endif() -add_library(trx src/trx.cpp src/trx.tpp src/trx.h) +add_library(trx src/trx.cpp include/trx/trx.h include/trx/trx.tpp) +add_library(trx-cpp::trx ALIAS trx) if(TRX_USE_CONAN AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ConanSetup.cmake") include(cmake/ConanSetup.cmake) @@ -37,21 +58,38 @@ elseif(TRX_USE_CONAN) message(STATUS "TRX_USE_CONAN enabled but cmake/ConanSetup.cmake not found; skipping Conan.") endif() +# Fallback for libzip packages that don't expose include dirs via CMake targets. +set(TRX_LIBZIP_INCLUDE_DIR "") +get_target_property(_trx_libzip_includes ${TRX_LIBZIP_TARGET} INTERFACE_INCLUDE_DIRECTORIES) +if(NOT _trx_libzip_includes) + find_path(TRX_LIBZIP_INCLUDE_DIR zip.h) + if(NOT TRX_LIBZIP_INCLUDE_DIR) + message(FATAL_ERROR "libzip headers not found. Set TRX_LIBZIP_INCLUDE_DIR or fix libzip CMake targets.") + endif() +endif() + TARGET_LINK_LIBRARIES(trx - PUBLIC - nlohmann_json::nlohmann_json - libzip::zip - Eigen3::Eigen + PUBLIC + nlohmann_json::nlohmann_json + ${TRX_LIBZIP_TARGET} + Eigen3::Eigen spdlog::spdlog - spdlog::spdlog_header_only + $<$:mio::mio> ) target_include_directories(trx PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${MIO_INCLUDE_DIR} + $ + $ + $<$>:$> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ) +if(TRX_LIBZIP_INCLUDE_DIR) + target_include_directories(trx PUBLIC + $ + $ + ) +endif() if(TRX_BUILD_TESTS) find_package(GTest CONFIG QUIET) @@ -66,6 +104,42 @@ if(TRX_BUILD_TESTS) endif() endif() +# Installation and package config +set(TRX_INSTALL_CONFIGDIR "${CMAKE_INSTALL_LIBDIR}/cmake/trx-cpp") + +install(TARGETS trx + EXPORT trx-cppTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +install(EXPORT trx-cppTargets + FILE trx-cppTargets.cmake + NAMESPACE trx-cpp:: + DESTINATION ${TRX_INSTALL_CONFIGDIR} +) + +configure_package_config_file( + cmake/trx-cppConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/trx-cppConfig.cmake + INSTALL_DESTINATION ${TRX_INSTALL_CONFIGDIR} +) + +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/trx-cppConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMinorVersion +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/trx-cppConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/trx-cppConfigVersion.cmake + DESTINATION ${TRX_INSTALL_CONFIGDIR} +) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) diff --git a/cmake/trx-cppConfig.cmake.in b/cmake/trx-cppConfig.cmake.in new file mode 100644 index 0000000..1b46b66 --- /dev/null +++ b/cmake/trx-cppConfig.cmake.in @@ -0,0 +1,13 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(libzip) +find_dependency(Eigen3) +find_dependency(nlohmann_json) +find_dependency(spdlog) +# mio is header-only; try to locate if packaged +find_dependency(mio QUIET) + +include("${CMAKE_CURRENT_LIST_DIR}/trx-cppTargets.cmake") + +check_required_components("trx-cpp") diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..3cf80bb --- /dev/null +++ b/conanfile.py @@ -0,0 +1,132 @@ +from conan import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout +from conan.tools.files import get, copy +import os + + +class TrxCppConan(ConanFile): + name = "trx-cpp" + version = "0.1.0" + package_type = "library" + license = "MIT" # Update if the project adopts a different license. + url = "https://github.com/tractdata/trx-cpp" + homepage = "https://github.com/tractdata/trx-cpp" + description = "C++ library for reading and writing the TRX tractography format." + topics = ("tractography", "mmap", "neuroimaging") + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + "with_tests": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + "with_tests": False, + } + generators = ("CMakeDeps",) + exports_sources = ( + "CMakeLists.txt", + "src/*", + "include/*", + "cmake/*", + "tests/*", + ) + + def config_options(self): + if self.settings.os == "Windows": + self.options.rm_safe("fPIC") + + def requirements(self): + self.requires("libzip/1.10.1") + self.requires("nlohmann_json/3.11.3") + self.requires("eigen/3.4.0") + self.requires("spdlog/1.12.0") + + def build_requirements(self): + if self.options.with_tests: + # Only needed for building/running tests, not for consumers. + self.test_requires("gtest/1.14.0") + + def layout(self): + cmake_layout(self) + + def source(self): + # Fetch header-only mio directly to avoid external package availability issues. + # Using the upstream main branch archive to ensure availability. + get( + self, + url="https://github.com/mandreyel/mio/archive/refs/heads/master.zip", + strip_root=True, + destination="mio", + ) + + def generate(self): + tc = CMakeToolchain(self) + mio_include = os.path.join(self.source_folder, "mio", "include") + tc.variables["MIO_INCLUDE_DIR"] = mio_include + tc.generate() + + def build(self): + cmake = CMake(self) + cmake.configure(variables={"TRX_BUILD_TESTS": self.options.with_tests}) + cmake.build() + if self.options.with_tests: + cmake.ctest() + + def package(self): + cmake = CMake(self) + cmake.install() + # Ensure libzip headers are available to consumers even when the + # libzip Conan package does not expose include dirs via CMakeDeps. + libzip_dep = self.dependencies.get("libzip") + if libzip_dep and libzip_dep.package_folder: + libzip_include = os.path.join(libzip_dep.package_folder, "include") + copy(self, "zip.h", src=libzip_include, dst=os.path.join(self.package_folder, "include")) + copy(self, "zipconf.h", src=libzip_include, dst=os.path.join(self.package_folder, "include")) + + nlohmann_dep = self.dependencies.get("nlohmann_json") + if nlohmann_dep and nlohmann_dep.package_folder: + nlohmann_include = os.path.join(nlohmann_dep.package_folder, "include") + copy(self, "nlohmann/*", src=nlohmann_include, dst=os.path.join(self.package_folder, "include")) + eigen_dep = self.dependencies.get("eigen") + if eigen_dep and eigen_dep.package_folder: + eigen_include = os.path.join(eigen_dep.package_folder, "include", "eigen3") + copy(self, "Eigen/*", src=eigen_include, dst=os.path.join(self.package_folder, "include")) + + mio_include = os.path.join(self.source_folder, "mio", "include") + copy(self, "mio/*", src=mio_include, dst=os.path.join(self.package_folder, "include")) + + spdlog_dep = self.dependencies.get("spdlog") + if spdlog_dep and spdlog_dep.package_folder: + spdlog_include = os.path.join(spdlog_dep.package_folder, "include") + copy(self, "spdlog/*", src=spdlog_include, dst=os.path.join(self.package_folder, "include")) + fmt_dep = self.dependencies.get("fmt") + if fmt_dep and fmt_dep.package_folder: + fmt_include = os.path.join(fmt_dep.package_folder, "include") + copy(self, "fmt/*", src=fmt_include, dst=os.path.join(self.package_folder, "include")) + + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "trx-cpp") + self.cpp_info.set_property("cmake_target_name", "trx-cpp::trx") + self.cpp_info.set_property("pkg_config_name", "trx-cpp") + self.cpp_info.components["trx"].set_property("cmake_target_name", "trx-cpp::trx") + self.cpp_info.components["trx"].set_property("pkg_config_name", "trx-cpp") + self.cpp_info.components["trx"].libs = ["trx"] + self.cpp_info.components["trx"].requires = [ + "libzip::libzip", + "spdlog::spdlog", + "nlohmann_json::nlohmann_json", + "eigen::Eigen3::Eigen", + ] + extra_includes = [] + for dep_name in ("nlohmann_json", "eigen"): + dep = self.dependencies.get(dep_name) + if dep and dep.package_folder: + dep_include = os.path.join(dep.package_folder, "include") + if os.path.isdir(dep_include): + extra_includes.append(dep_include) + if extra_includes: + self.cpp_info.components["trx"].includedirs.extend(extra_includes) + if not self.options.shared and self.settings.os in ("Linux", "FreeBSD"): + self.cpp_info.system_libs.append("pthread") diff --git a/include/trx/trx.h b/include/trx/trx.h index b8de775..f4d0c79 100644 --- a/include/trx/trx.h +++ b/include/trx/trx.h @@ -1,10 +1,309 @@ -#pragma once +#ifndef TRX_H // include guard +#define TRX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include -// Provide the public include path expected by consumers. -// This wrapper forwards to the actual header in src/ while -// also ensuring SPDLOG uses the external fmt if provided. #ifndef SPDLOG_FMT_EXTERNAL #define SPDLOG_FMT_EXTERNAL #endif +#include "spdlog/spdlog.h" + +using namespace Eigen; +using json = nlohmann::json; + +namespace trxmmap +{ + + const std::string SEPARATOR = "/"; + const std::vector dtypes({"float16", "bit", "uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"}); + + template + struct ArraySequence + { + Map> _data; + Map> _offsets; + Matrix _lengths; + mio::shared_mmap_sink mmap_pos; + mio::shared_mmap_sink mmap_off; + + ArraySequence() : _data(NULL, 1, 1), _offsets(NULL, 1, 1){}; + }; + + template + struct MMappedMatrix + { + Map> _matrix; + mio::shared_mmap_sink mmap; + + MMappedMatrix() : _matrix(NULL, 1, 1){}; + }; + + template + class TrxFile + { + // Access specifier + public: + // Data Members + json header; + ArraySequence
*streamlines; + + std::map *> groups; // vector of indices + + // int or float --check python floa *> data_per_streamline; + std::map *> data_per_vertex; + std::map *>> data_per_group; + std::string _uncompressed_folder_handle; + bool _copy_safe; + bool _owns_uncompressed_folder = false; + + // Member Functions() + // TrxFile(int nb_vertices = 0, int nb_streamlines = 0); + TrxFile(int nb_vertices = 0, int nb_streamlines = 0, const TrxFile
*init_as = NULL, std::string reference = ""); + ~TrxFile(); + + /** + * @brief After reading the structure of a zip/folder, create a TrxFile + * + * @param header A TrxFile header dictionary which will be used for the new TrxFile + * @param dict_pointer_size A dictionary containing the filenames of all the files within the TrxFile disk file/folder + * @param root_zip The path of the ZipFile pointer + * @param root The dirname of the ZipFile pointer + * @return TrxFile* + */ + static TrxFile
*_create_trx_from_pointer(json header, std::map> dict_pointer_size, std::string root_zip = "", std::string root = ""); + + /** + * @brief Create a deepcopy of the TrxFile + * + * @return TrxFile
* A deepcopied TrxFile of the current object + */ + TrxFile
*deepcopy(); + + /** + * @brief Remove the ununsed portion of preallocated memmaps + * + * @param nb_streamlines The number of streamlines to keep + * @param nb_vertices The number of vertices to keep + * @param delete_dpg Remove data_per_group when resizing + */ + void resize(int nb_streamlines = -1, int nb_vertices = -1, bool delete_dpg = false); + + /** + * @brief Cleanup on-disk temporary folder and initialize an empty TrxFile + * + */ + void close(); + void _cleanup_temporary_directory(); + + private: + /** + * @brief Get the real size of data (ignoring zeros of preallocation) + * + * @return std::tuple A tuple representing the index of the last streamline and the total length of all the streamlines + */ + std::tuple _get_real_len(); + + /** + * @brief Fill a TrxFile using another and start indexes (preallocation) + * + * @param trx TrxFile to copy data from + * @param strs_start The start index of the streamline + * @param pts_start The start index of the point + * @param nb_strs_to_copy The number of streamlines to copy. If not set will copy all + * @return std::tuple A tuple representing the end of the copied streamlines and end of copied points + */ + std::tuple _copy_fixed_arrays_from(TrxFile
*trx, int strs_start = 0, int pts_start = 0, int nb_strs_to_copy = -1); + int len(); + }; + + /** + * TODO: This function might be completely unecessary + * + * @param[in] root a Json::Value root obtained from reading a header file with JsonCPP + * @param[out] header a header containing the same elements as the original root + * */ + json assignHeader(json root); + + /** + * Returns the properly formatted datatype name + * + * @param[in] dtype the returned Eigen datatype + * @param[out] fmt_dtype the formatted datatype + * + * */ + std::string _get_dtype(std::string dtype); + + /** + * @brief Get the size of the datatype + * + * @param dtype the string name of the datatype + * @return int corresponding to the size of the datatype + */ + int _sizeof_dtype(std::string dtype); + + /** + * Determine whether the extension is a valid extension + * + * + * @param[in] ext a string consisting of the extension starting by a . + * @param[out] is_valid a boolean denoting whether the extension is valid. + * + * */ + bool _is_dtype_valid(std::string &ext); + + /** + * This function loads the header json file + * stored within a Zip archive + * + * @param[in] zfolder a pointer to an opened zip archive + * @param[out] header the JSONCpp root of the header. NULL on error. + * + * */ + json load_header(zip_t *zfolder); + + /** + * Load the TRX file stored within a Zip archive. + * + * @param[in] path path to Zip archive + * @param[out] status return 0 if success else 1 + * + * */ + template + TrxFile
*load_from_zip(std::string path); + + /** + * @brief Load a TrxFile from a folder containing memmaps + * + * @tparam DT + * @param path path of the zipped TrxFile + * @return TrxFile
* TrxFile representing the read data + */ + template + TrxFile
*load_from_directory(std::string path); + + /** + * Get affine and dimensions from a Nifti or Trk file (Adapted from dipy) + * + * @param[in] reference a string pointing to a NIfTI or trk file to be used as reference + * @param[in] affine 4x4 affine matrix + * @param[in] dimensions vector of size 3 + * + * */ + void get_reference_info(std::string reference, const MatrixXf &affine, const RowVectorXf &dimensions); + + template + std::ostream &operator<<(std::ostream &out, const TrxFile
&trx); + // private: + + void allocate_file(const std::string &path, const int size); + + /** + * @brief Wrapper to support empty array as memmaps + * + * @param filename filename of the file where the empty memmap should be created + * @param shape shape of memmapped NDArray + * @param mode file open mode + * @param dtype datatype of memmapped NDArray + * @param offset offset of the data within the file + * @return mio::shared_mmap_sink + */ + // TODO: ADD order?? + // TODO: change tuple to vector to support ND arrays? + // TODO: remove data type as that's done outside of this function + mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple &shape, std::string mode = "r", std::string dtype = "float32", long long offset = 0); + + template + std::string _generate_filename_from_data(const MatrixBase
&arr, const std::string filename); + std::tuple _split_ext_with_dimensionality(const std::string filename); + + /** + * @brief Compute the lengths from offsets and header information + * + * @tparam DT The datatype (used for the input matrix) + * @param[in] offsets An array of offsets + * @param[in] nb_vertices the number of vertices + * @return Matrix of lengths + */ + template + Matrix _compute_lengths(const MatrixBase
&offsets, int nb_vertices); + + /** + * @brief Find where data of a contiguous array is actually ending + * + * @tparam DT (the datatype) + * @param x Matrix of values + * @param l_bound lower bound index for search + * @param r_bound upper bound index for search + * @return int index at which array value is 0 (if possible), otherwise returns -1 + */ + template + int _dichotomic_search(const MatrixBase
&x, int l_bound = -1, int r_bound = -1); + + /** + * @brief Create on-disk memmaps of a certain size (preallocation) + * + * @param nb_streamlines The number of streamlines that the empty TrxFile will be initialized with + * @param nb_vertices The number of vertices that the empty TrxFile will be initialized with + * @param init_as A TrxFile to initialize the empty TrxFile with + * @return TrxFile
An empty TrxFile preallocated with a certain size + */ + template + TrxFile
*_initialize_empty_trx(int nb_streamlines, int nb_vertices, const TrxFile
*init_as = NULL); + + template + void ediff1d(Matrix &lengths, const Matrix &tmp, uint32_t to_end); + + /** + * @brief Save a TrxFile + * + * @tparam DT + * @param trx The TrxFile to save + * @param filename The path to save the TrxFile to + * @param compression_standard The compression standard to use, as defined by libzip (default: no compression) + */ + template + void save(TrxFile
&trx, const std::string filename, zip_uint32_t compression_standard = ZIP_CM_STORE); + + /** + * @brief Utils function to zip on-disk memmaps + * + * @param directory The path to the on-disk memmap + * @param filename The path where the zip file should be created + * @param compression_standard The compression standard to use, as defined by the ZipFile library + */ + void zip_from_folder(zip_t *zf, const std::string root, const std::string directory, zip_uint32_t compression_standard = ZIP_CM_STORE); + + std::string get_base(const std::string &delimiter, const std::string &str); + std::string get_ext(const std::string &str); + void populate_fps(const char *name, std::map> &file_pointer_size); + + void copy_dir(const char *src, const char *dst); + void copy_file(const char *src, const char *dst); + int rm_dir(const char *d); + std::string make_temp_dir(const std::string &prefix); + std::string extract_zip_to_directory(zip_t *zfolder); + + std::string rm_root(std::string root, const std::string path); +#include + +} -#include "../../src/trx.h" +#endif /* TRX_H */ \ No newline at end of file diff --git a/src/trx.tpp b/include/trx/trx.tpp similarity index 100% rename from src/trx.tpp rename to include/trx/trx.tpp diff --git a/src/trx.cpp b/src/trx.cpp index 9390a14..127cc07 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -1,4 +1,4 @@ -#include "trx.h" +#include #include #include #include diff --git a/src/trx.h b/src/trx.h deleted file mode 100644 index 94c9585..0000000 --- a/src/trx.h +++ /dev/null @@ -1,309 +0,0 @@ -#ifndef TRX_H // include guard -#define TRX_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#ifndef SPDLOG_FMT_EXTERNAL -#define SPDLOG_FMT_EXTERNAL -#endif -#include "spdlog/spdlog.h" - -using namespace Eigen; -using json = nlohmann::json; - -namespace trxmmap -{ - - const std::string SEPARATOR = "/"; - const std::vector dtypes({"float16", "bit", "uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"}); - - template - struct ArraySequence - { - Map> _data; - Map> _offsets; - Matrix _lengths; - mio::shared_mmap_sink mmap_pos; - mio::shared_mmap_sink mmap_off; - - ArraySequence() : _data(NULL, 1, 1), _offsets(NULL, 1, 1){}; - }; - - template - struct MMappedMatrix - { - Map> _matrix; - mio::shared_mmap_sink mmap; - - MMappedMatrix() : _matrix(NULL, 1, 1){}; - }; - - template - class TrxFile - { - // Access specifier - public: - // Data Members - json header; - ArraySequence
*streamlines; - - std::map *> groups; // vector of indices - - // int or float --check python floa *> data_per_streamline; - std::map *> data_per_vertex; - std::map *>> data_per_group; - std::string _uncompressed_folder_handle; - bool _copy_safe; - bool _owns_uncompressed_folder = false; - - // Member Functions() - // TrxFile(int nb_vertices = 0, int nb_streamlines = 0); - TrxFile(int nb_vertices = 0, int nb_streamlines = 0, const TrxFile
*init_as = NULL, std::string reference = ""); - ~TrxFile(); - - /** - * @brief After reading the structure of a zip/folder, create a TrxFile - * - * @param header A TrxFile header dictionary which will be used for the new TrxFile - * @param dict_pointer_size A dictionary containing the filenames of all the files within the TrxFile disk file/folder - * @param root_zip The path of the ZipFile pointer - * @param root The dirname of the ZipFile pointer - * @return TrxFile* - */ - static TrxFile
*_create_trx_from_pointer(json header, std::map> dict_pointer_size, std::string root_zip = "", std::string root = ""); - - /** - * @brief Create a deepcopy of the TrxFile - * - * @return TrxFile
* A deepcopied TrxFile of the current object - */ - TrxFile
*deepcopy(); - - /** - * @brief Remove the ununsed portion of preallocated memmaps - * - * @param nb_streamlines The number of streamlines to keep - * @param nb_vertices The number of vertices to keep - * @param delete_dpg Remove data_per_group when resizing - */ - void resize(int nb_streamlines = -1, int nb_vertices = -1, bool delete_dpg = false); - - /** - * @brief Cleanup on-disk temporary folder and initialize an empty TrxFile - * - */ - void close(); - void _cleanup_temporary_directory(); - - private: - /** - * @brief Get the real size of data (ignoring zeros of preallocation) - * - * @return std::tuple A tuple representing the index of the last streamline and the total length of all the streamlines - */ - std::tuple _get_real_len(); - - /** - * @brief Fill a TrxFile using another and start indexes (preallocation) - * - * @param trx TrxFile to copy data from - * @param strs_start The start index of the streamline - * @param pts_start The start index of the point - * @param nb_strs_to_copy The number of streamlines to copy. If not set will copy all - * @return std::tuple A tuple representing the end of the copied streamlines and end of copied points - */ - std::tuple _copy_fixed_arrays_from(TrxFile
*trx, int strs_start = 0, int pts_start = 0, int nb_strs_to_copy = -1); - int len(); - }; - - /** - * TODO: This function might be completely unecessary - * - * @param[in] root a Json::Value root obtained from reading a header file with JsonCPP - * @param[out] header a header containing the same elements as the original root - * */ - json assignHeader(json root); - - /** - * Returns the properly formatted datatype name - * - * @param[in] dtype the returned Eigen datatype - * @param[out] fmt_dtype the formatted datatype - * - * */ - std::string _get_dtype(std::string dtype); - - /** - * @brief Get the size of the datatype - * - * @param dtype the string name of the datatype - * @return int corresponding to the size of the datatype - */ - int _sizeof_dtype(std::string dtype); - - /** - * Determine whether the extension is a valid extension - * - * - * @param[in] ext a string consisting of the extension starting by a . - * @param[out] is_valid a boolean denoting whether the extension is valid. - * - * */ - bool _is_dtype_valid(std::string &ext); - - /** - * This function loads the header json file - * stored within a Zip archive - * - * @param[in] zfolder a pointer to an opened zip archive - * @param[out] header the JSONCpp root of the header. NULL on error. - * - * */ - json load_header(zip_t *zfolder); - - /** - * Load the TRX file stored within a Zip archive. - * - * @param[in] path path to Zip archive - * @param[out] status return 0 if success else 1 - * - * */ - template - TrxFile
*load_from_zip(std::string path); - - /** - * @brief Load a TrxFile from a folder containing memmaps - * - * @tparam DT - * @param path path of the zipped TrxFile - * @return TrxFile
* TrxFile representing the read data - */ - template - TrxFile
*load_from_directory(std::string path); - - /** - * Get affine and dimensions from a Nifti or Trk file (Adapted from dipy) - * - * @param[in] reference a string pointing to a NIfTI or trk file to be used as reference - * @param[in] affine 4x4 affine matrix - * @param[in] dimensions vector of size 3 - * - * */ - void get_reference_info(std::string reference, const MatrixXf &affine, const RowVectorXf &dimensions); - - template - std::ostream &operator<<(std::ostream &out, const TrxFile
&trx); - // private: - - void allocate_file(const std::string &path, const int size); - - /** - * @brief Wrapper to support empty array as memmaps - * - * @param filename filename of the file where the empty memmap should be created - * @param shape shape of memmapped NDArray - * @param mode file open mode - * @param dtype datatype of memmapped NDArray - * @param offset offset of the data within the file - * @return mio::shared_mmap_sink - */ - // TODO: ADD order?? - // TODO: change tuple to vector to support ND arrays? - // TODO: remove data type as that's done outside of this function - mio::shared_mmap_sink _create_memmap(std::string filename, std::tuple &shape, std::string mode = "r", std::string dtype = "float32", long long offset = 0); - - template - std::string _generate_filename_from_data(const MatrixBase
&arr, const std::string filename); - std::tuple _split_ext_with_dimensionality(const std::string filename); - - /** - * @brief Compute the lengths from offsets and header information - * - * @tparam DT The datatype (used for the input matrix) - * @param[in] offsets An array of offsets - * @param[in] nb_vertices the number of vertices - * @return Matrix of lengths - */ - template - Matrix _compute_lengths(const MatrixBase
&offsets, int nb_vertices); - - /** - * @brief Find where data of a contiguous array is actually ending - * - * @tparam DT (the datatype) - * @param x Matrix of values - * @param l_bound lower bound index for search - * @param r_bound upper bound index for search - * @return int index at which array value is 0 (if possible), otherwise returns -1 - */ - template - int _dichotomic_search(const MatrixBase
&x, int l_bound = -1, int r_bound = -1); - - /** - * @brief Create on-disk memmaps of a certain size (preallocation) - * - * @param nb_streamlines The number of streamlines that the empty TrxFile will be initialized with - * @param nb_vertices The number of vertices that the empty TrxFile will be initialized with - * @param init_as A TrxFile to initialize the empty TrxFile with - * @return TrxFile
An empty TrxFile preallocated with a certain size - */ - template - TrxFile
*_initialize_empty_trx(int nb_streamlines, int nb_vertices, const TrxFile
*init_as = NULL); - - template - void ediff1d(Matrix &lengths, const Matrix &tmp, uint32_t to_end); - - /** - * @brief Save a TrxFile - * - * @tparam DT - * @param trx The TrxFile to save - * @param filename The path to save the TrxFile to - * @param compression_standard The compression standard to use, as defined by libzip (default: no compression) - */ - template - void save(TrxFile
&trx, const std::string filename, zip_uint32_t compression_standard = ZIP_CM_STORE); - - /** - * @brief Utils function to zip on-disk memmaps - * - * @param directory The path to the on-disk memmap - * @param filename The path where the zip file should be created - * @param compression_standard The compression standard to use, as defined by the ZipFile library - */ - void zip_from_folder(zip_t *zf, const std::string root, const std::string directory, zip_uint32_t compression_standard = ZIP_CM_STORE); - - std::string get_base(const std::string &delimiter, const std::string &str); - std::string get_ext(const std::string &str); - void populate_fps(const char *name, std::map> &file_pointer_size); - - void copy_dir(const char *src, const char *dst); - void copy_file(const char *src, const char *dst); - int rm_dir(const char *d); - std::string make_temp_dir(const std::string &prefix); - std::string extract_zip_to_directory(zip_t *zfolder); - - std::string rm_root(std::string root, const std::string path); -#include "trx.tpp" - -} - -#endif /* TRX_H */ \ No newline at end of file diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt new file mode 100644 index 0000000..d463faa --- /dev/null +++ b/test_package/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.15) +project(trx_cpp_test_package LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(trx-cpp CONFIG REQUIRED) + +add_executable(example src/example.cpp) +target_link_libraries(example PRIVATE trx-cpp::trx) diff --git a/test_package/conanfile.py b/test_package/conanfile.py new file mode 100644 index 0000000..d9d2a8a --- /dev/null +++ b/test_package/conanfile.py @@ -0,0 +1,25 @@ +import os + +from conan import ConanFile +from conan.tools.cmake import CMake, cmake_layout + + +class TrxCppTestPackage(ConanFile): + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeDeps", "CMakeToolchain" + test_type = "explicit" + + def requirements(self): + self.requires(self.tested_reference_str) + + def layout(self): + cmake_layout(self) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def test(self): + exe_name = "example.exe" if self.settings.os == "Windows" else "example" + self.run(os.path.join(self.build_folder, exe_name), env="conanrun") diff --git a/test_package/src/example.cpp b/test_package/src/example.cpp new file mode 100644 index 0000000..7c52822 --- /dev/null +++ b/test_package/src/example.cpp @@ -0,0 +1,9 @@ +#include + +int main() +{ + // Basic construction and cleanup exercises the public API and linkage. + trxmmap::TrxFile file; + file.close(); + return 0; +}