From 11d0c8106ff88454d67d4ea96aadcc36d9ff2921 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Fri, 17 Jul 2026 14:02:35 -0500 Subject: [PATCH] BUG: Read the extension from the final path component in get_ext get_ext() scanned the entire path for '.' via rfind, so a dot anywhere in a parent directory was treated as the file extension. A common trigger is a version-numbered build/output path: for ".../build-5.4/.../trx_test" (a file with no extension) get_ext returned "4/.../trx_test", which the writer rejects with "Unsupported extension". Restrict the search to the final path component (after the last '/' or '\'), then take the substring after its last '.'. A file with no dot in its name now correctly yields an empty extension. This does not change the dtype-extraction callers: array element filenames such as "positions.3.float32" and "offsets.uint64" carry the dot in the basename, so the returned dtype is unchanged. Adds TrxFileIo.get_ext_uses_final_path_component covering the dotted-parent regression, no-extension, no-separator, dtype-filename, and trailing-dot cases. Full ctest suite passes 185/185. Found via an ITK v5-vs-v6 downstream build testbed whose build path contained a dotted directory; the three ITKTractographyTRX tests that failed there pass with this fix. --- src/trx.cpp | 13 +++++++++---- tests/test_trx_io.cpp | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/trx.cpp b/src/trx.cpp index 869a734..d53d207 100644 --- a/src/trx.cpp +++ b/src/trx.cpp @@ -849,12 +849,17 @@ std::string get_base(const std::string &delimiter, const std::string &str) { } std::string get_ext(const std::string &str) { + // Extract the extension from the final path component only. Scanning the + // whole path for '.' mistakes a dot in a parent directory (e.g. a + // version-numbered build path like ".../foo-5.4/out") for the extension. + const std::size_t sep = str.find_last_of("/\\"); + const std::string name = (sep == std::string::npos) ? str : str.substr(sep + 1); + std::string ext; constexpr char kDelimiter = '.'; - - const std::size_t pos = str.rfind(kDelimiter); - if (pos != std::string::npos && pos + 1 < str.length()) { - ext = str.substr(pos + 1); + const std::size_t pos = name.rfind(kDelimiter); + if (pos != std::string::npos && pos + 1 < name.length()) { + ext = name.substr(pos + 1); } return ext; } diff --git a/tests/test_trx_io.cpp b/tests/test_trx_io.cpp index 2c5bac0..b03e6ae 100644 --- a/tests/test_trx_io.cpp +++ b/tests/test_trx_io.cpp @@ -839,3 +839,21 @@ TEST(TrxFileIo, export_dpv_to_tsf_errors) { trx::TrxFile empty; EXPECT_THROW(empty.export_dpv_to_tsf("signal", output_path.string(), "1"), trx::TrxFormatError); } + +// get_ext must read the extension from the final path component only. A '.' in +// a parent directory (e.g. a version-numbered build path like ".../foo-5.4/") +// must not be treated as the extension. Regression for "Unsupported extension". +TEST(TrxFileIo, get_ext_uses_final_path_component) { + // Dotted parent directory, file has no extension -> empty (the bug case). + EXPECT_EQ(trx::get_ext("/a/build-5.4/out/trx_test"), ""); + EXPECT_EQ(trx::get_ext("/a/build-5.4/out/trx_test.trx"), "trx"); + // No path separator: whole string is the name. + EXPECT_EQ(trx::get_ext("out.trx"), "trx"); + EXPECT_EQ(trx::get_ext("trx_test"), ""); + // dtype-style array filenames (name.dims.dtype) resolve to the dtype, even + // under a dotted parent directory. + EXPECT_EQ(trx::get_ext("/a/shard.1/positions.3.float32"), "float32"); + EXPECT_EQ(trx::get_ext("offsets.uint64"), "uint64"); + // Trailing dot -> empty. + EXPECT_EQ(trx::get_ext("/a/b.c/name."), ""); +}