From e5dbc33fa6a002a5ddbb083452419c169cb2c12d Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Thu, 10 Sep 2026 10:59:51 -0600 Subject: [PATCH 1/3] Make file existence checks agree with the mesh and image readers boost::filesystem::is_regular_file() opens a file handle on Windows and reports false whenever that fails, so files that VTK and ITK read without trouble were rejected before the reader ever saw them. Use vtksys for the existence checks, which queries attributes and only opens a handle for reparse points, and imbue boost's path with a UTF-8 codecvt at startup so the remaining boost calls stop mangling non-ASCII paths. Also makes Project::set_project_path tolerant when a path can't be re-anchored to a new project location, so one awkward path no longer silently skips the rewrite or fails the entire save. --- Applications/shapeworks/shapeworks.cpp | 3 +++ Libs/Analyze/Shape.cpp | 3 ++- Libs/Common/ShapeworksUtils.cpp | 28 ++++++++++++++++--- Libs/Common/ShapeworksUtils.h | 11 +++++--- Libs/Image/Image.cpp | 2 +- Libs/Project/Project.cpp | 37 +++++++++++++++----------- Libs/Python/ShapeworksPython.cpp | 2 ++ Studio/main.cpp | 3 +++ Testing/CMakeLists.txt | 1 + Testing/MeshTests/MeshTests.cpp | 36 +++++++++++++++++++++++++ Testing/Testing.cpp | 4 ++- 11 files changed, 105 insertions(+), 25 deletions(-) diff --git a/Applications/shapeworks/shapeworks.cpp b/Applications/shapeworks/shapeworks.cpp index 16acbba8f43..ea12ab45297 100644 --- a/Applications/shapeworks/shapeworks.cpp +++ b/Applications/shapeworks/shapeworks.cpp @@ -6,11 +6,14 @@ #include "Executable.h" #include "Commands.h" #include +#include using namespace shapeworks; int main(int argc, char *argv[]) { + ShapeWorksUtils::initialize_path_handling(); + Executable shapeworks; // Image Commands diff --git a/Libs/Analyze/Shape.cpp b/Libs/Analyze/Shape.cpp index 2fa8ef3fda4..4f9a74670c3 100644 --- a/Libs/Analyze/Shape.cpp +++ b/Libs/Analyze/Shape.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -912,7 +913,7 @@ void Shape::set_reconstruction_transforms(std::vector #include #include +#include #include +#include +#include namespace shapeworks { @@ -19,16 +22,33 @@ void ShapeWorksUtils::set_rng_seed(const unsigned seed) { mt_.seed(rng_seed_); } +//----------------------------------------------------------------------------- +void ShapeWorksUtils::initialize_path_handling() { + // boost::filesystem stores a path in the character type the OS API uses: char on POSIX, but + // wchar_t on Windows. There every narrow string is converted using the codecvt facet imbued + // into boost::filesystem::path, which by default comes from the global locale rather than being + // UTF-8. The rest of ShapeWorks (Qt, VTK, ITK) speaks UTF-8, so without this a path holding + // non-ASCII characters is mangled on the way in and no longer resolves. On POSIX no conversion + // takes place and this only affects the wide-string accessors. + boost::filesystem::path::imbue(std::locale(std::locale(), new boost::nowide::utf8_codecvt)); +} + //----------------------------------------------------------------------------- bool ShapeWorksUtils::is_directory(const std::string& pathname) { - boost::system::error_code ec; - return boost::filesystem::is_directory(pathname, ec); + return vtksys::SystemTools::FileIsDirectory(pathname); } //----------------------------------------------------------------------------- bool ShapeWorksUtils::file_exists(const std::string& filename) { - boost::system::error_code ec; - return boost::filesystem::is_regular_file(filename, ec); + // vtksys rather than boost::filesystem: on Windows it decodes UTF-8 and applies the \\?\ prefix, + // so it agrees with the VTK and ITK readers about which paths exist. boost::filesystem does + // neither, and rejects long or non-ASCII paths that those readers open without complaint. + return vtksys::SystemTools::FileExists(filename, true); +} + +//----------------------------------------------------------------------------- +bool ShapeWorksUtils::path_exists(const std::string& pathname) { + return vtksys::SystemTools::PathExists(pathname); } //----------------------------------------------------------------------------- diff --git a/Libs/Common/ShapeworksUtils.h b/Libs/Common/ShapeworksUtils.h index 84e0414e917..d4f5cf3a72f 100644 --- a/Libs/Common/ShapeworksUtils.h +++ b/Libs/Common/ShapeworksUtils.h @@ -31,14 +31,19 @@ class ShapeWorksUtils { /// generates random number static unsigned get_random_number() { return mt_(); } + //! Configure process-wide filesystem path handling. Call this once at startup, before any path + //! is constructed from a narrow string. + static void initialize_path_handling(); + /// returns true if pathname is a directory - // TODO: in C++17 this is a standard function static bool is_directory(const std::string& pathname); - /// returns true if filename exists - // TODO: in C++17 this is a standard function + /// returns true if filename exists and can be read static bool file_exists(const std::string& filename); + /// returns true if a file or a directory exists at pathname + static bool path_exists(const std::string& pathname); + //! Set up the console logging options static void setup_console_logging(bool show_progress, bool xml_status); diff --git a/Libs/Image/Image.cpp b/Libs/Image/Image.cpp index 63c66bb089f..235777a5def 100644 --- a/Libs/Image/Image.cpp +++ b/Libs/Image/Image.cpp @@ -117,7 +117,7 @@ Image::ImageType::Pointer Image::read(const std::string& pathname) { } // check if it exists - if (!boost::filesystem::exists(pathname)) { + if (!ShapeWorksUtils::file_exists(pathname)) { throw std::runtime_error("File does not exist: " + pathname); } diff --git a/Libs/Project/Project.cpp b/Libs/Project/Project.cpp index 289e5638749..db3d28f4f21 100644 --- a/Libs/Project/Project.cpp +++ b/Libs/Project/Project.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -91,17 +92,27 @@ void Project::set_project_path(const std::string& new_pathname) { auto old_path = fs::path(project_path_); auto new_path = fs::path(new_pathname); + // Re-anchor one path to the new project location. Returns the path unchanged if it can't be, + // so that a single awkward path doesn't fail the whole save. + auto rebase = [&](std::string path) { + if (!ShapeWorksUtils::path_exists(path)) { + return path; + } + // replace \ with / in path + path = StringUtils::replace_string(path, "\\", "/"); + try { + auto canonical = fs::canonical(path, old_path); + return fs::relative(canonical, new_path).string(); + } catch (std::exception& e) { + SW_WARN("Unable to update path \"{}\" for new project location: {}", path, e.what()); + return path; + } + }; + auto fixup = [&](StringList paths) { StringList new_paths; - for (auto path : paths) { - if (fs::exists(path)) { - // replace \ with / in path - path = StringUtils::replace_string(path, "\\", "/"); - auto canonical = fs::canonical(path, old_path); - new_paths.push_back(fs::relative(canonical, new_path).string()); - } else { - new_paths.push_back(path); - } + for (auto& path : paths) { + new_paths.push_back(rebase(path)); } return new_paths; }; @@ -117,11 +128,7 @@ void Project::set_project_path(const std::string& new_pathname) { auto features = subject->get_feature_filenames(); project::types::StringMap new_features; for (auto const& x : features) { - auto path = x.second; - // replace \ with / in path - path = StringUtils::replace_string(path, "\\", "/"); - auto canonical = fs::canonical(path, old_path); - new_features[x.first] = fs::relative(canonical, new_path).string(); + new_features[x.first] = rebase(x.second); } subject->set_feature_filenames(new_features); } @@ -350,7 +357,7 @@ void Project::determine_feature_names() { if (get_original_domain_types()[d] == DomainType::Mesh) { if (subject->get_original_filenames().size() > d) { auto filename = subject->get_original_filenames()[d]; - if (fs::exists(filename)) { + if (ShapeWorksUtils::file_exists(filename)) { try { auto poly_data = MeshUtils::threadSafeReadMesh(filename).getVTKMesh(); if (poly_data) { diff --git a/Libs/Python/ShapeworksPython.cpp b/Libs/Python/ShapeworksPython.cpp index ef37807450d..dc0c188cf71 100644 --- a/Libs/Python/ShapeworksPython.cpp +++ b/Libs/Python/ShapeworksPython.cpp @@ -59,6 +59,8 @@ namespace fs = boost::filesystem; using namespace shapeworks; PYBIND11_MODULE(shapeworks_py, m) { + ShapeWorksUtils::initialize_path_handling(); + m.doc() = "ShapeWorks Python API"; m.attr("Pi") = std::atan(1.0) * 4.0; diff --git a/Studio/main.cpp b/Studio/main.cpp index 5947ebb94c8..50175efd46d 100644 --- a/Studio/main.cpp +++ b/Studio/main.cpp @@ -19,6 +19,7 @@ // vtk #include #include +#include #include #include #include @@ -83,6 +84,8 @@ int main(int argc, char** argv) { TIME_SCOPE("ShapeWorksStudio"); try { + ShapeWorksUtils::initialize_path_handling(); + #ifdef Q_OS_MACOS // Prevent cursor crashes on Apple Silicon qputenv("QT_MAC_DISABLE_NATIVE_CURSORS", "1"); diff --git a/Testing/CMakeLists.txt b/Testing/CMakeLists.txt index 58dfb2fe165..7581bc6361a 100644 --- a/Testing/CMakeLists.txt +++ b/Testing/CMakeLists.txt @@ -55,6 +55,7 @@ target_include_directories(Testing PUBLIC target_link_libraries(Testing gtest gtest_main + Common ${Boost_LIBRARIES} ) set_target_properties(Testing PROPERTIES PUBLIC_HEADER diff --git a/Testing/MeshTests/MeshTests.cpp b/Testing/MeshTests/MeshTests.cpp index ca2b730a667..aba6c819c7e 100644 --- a/Testing/MeshTests/MeshTests.cpp +++ b/Testing/MeshTests/MeshTests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ #include "MeshWarper.h" #include "ParticleSystem.h" #include "ParticleSystemEvaluation.h" +#include "ShapeworksUtils.h" #include "Testing.h" using namespace shapeworks; @@ -1163,3 +1165,37 @@ TEST(MeshTests, extractLargestComponentSingleComponentTest) { ASSERT_EQ(femur.numPoints(), original_points); ASSERT_EQ(femur.numFaces(), original_faces); } + +TEST(MeshTests, nonAsciiPathTest) { + // Mesh's existence check has to agree with the reader underneath it about which paths are + // readable. On Windows boost::filesystem did not, and meshes that VTK opened without trouble + // were rejected as missing before the reader ever saw them. (#2648) + auto dir = TestUtils::Instance().get_output_dir("non_ascii_path_test"); + std::string unicode_dir = dir + "/Ünïcode Dätä"; + ASSERT_TRUE(vtksys::SystemTools::MakeDirectory(unicode_dir)); + + std::string path = unicode_dir + "/mesh.vtk"; + Mesh(std::string(TEST_DATA_DIR) + "/femur.vtk").write(path); + + ASSERT_TRUE(ShapeWorksUtils::file_exists(path)); + ASSERT_NO_THROW(Mesh mesh(path)); +} + +TEST(MeshTests, longPathTest) { + // Paths beyond MAX_PATH need the \\?\ prefix on Windows. The readers apply it; the existence + // check in front of them did not. (#2648) + auto dir = TestUtils::Instance().get_output_dir("long_path_test"); + + std::string deep = dir; + while (deep.size() < 300) { + deep += "/a_directory_with_a_deliberately_long_name"; + } + ASSERT_TRUE(vtksys::SystemTools::MakeDirectory(deep)); + + std::string path = deep + "/mesh.vtk"; + ASSERT_GT(path.size(), 260u); + Mesh(std::string(TEST_DATA_DIR) + "/femur.vtk").write(path); + + ASSERT_TRUE(ShapeWorksUtils::file_exists(path)); + ASSERT_NO_THROW(Mesh mesh(path)); +} diff --git a/Testing/Testing.cpp b/Testing/Testing.cpp index 2e890c7906d..58437793eec 100644 --- a/Testing/Testing.cpp +++ b/Testing/Testing.cpp @@ -1,5 +1,7 @@ #include "Testing.h" +#include + #include #include @@ -31,7 +33,7 @@ TestUtils& TestUtils::Instance() { } //----------------------------------------------------------------------------- -TestUtils::TestUtils() {} +TestUtils::TestUtils() { ShapeWorksUtils::initialize_path_handling(); } //----------------------------------------------------------------------------- TestUtils::~TestUtils() { From 289d8c70099c896b8c9ad990ae1c7b8e1e087d97 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Thu, 10 Sep 2026 15:32:12 -0600 Subject: [PATCH 2/3] Suppress boost::nowide auto-linking on MSVC Including boost/nowide/utf8_codecvt.hpp emits the auto-link pragma for boost_nowide.lib, which the Windows dependency build does not produce, failing the link. The codecvt facet is entirely header-only, so define BOOST_NOWIDE_NO_LIB ahead of every Boost header to opt out of auto-linking. --- Libs/Common/ShapeworksUtils.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Libs/Common/ShapeworksUtils.cpp b/Libs/Common/ShapeworksUtils.cpp index f01befa5f60..99799c8cfab 100644 --- a/Libs/Common/ShapeworksUtils.cpp +++ b/Libs/Common/ShapeworksUtils.cpp @@ -1,3 +1,8 @@ +// boost::nowide's utf8_codecvt is header-only, but including it pulls in the auto-link pragma +// that asks MSVC for boost_nowide.lib, which the dependency build does not produce. This must +// precede every Boost header. +#define BOOST_NOWIDE_NO_LIB + #include "ShapeworksUtils.h" #include From 4eb25467eb73de34a7edce4ac40e5c3df92c9c11 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Thu, 10 Sep 2026 18:13:27 -0600 Subject: [PATCH 3/3] Use STL for the long path test VTK's legacy .vtk reader stats the file before opening it, and on Windows that stat deliberately omits the \\?\ prefix, so a path beyond MAX_PATH can be written but never read back. The STL reader and writer go through SystemTools::Fopen, which applies the prefix, so they exercise what the test is actually about: that the existence check no longer rejects such a path. --- Testing/MeshTests/MeshTests.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Testing/MeshTests/MeshTests.cpp b/Testing/MeshTests/MeshTests.cpp index aba6c819c7e..572f876b328 100644 --- a/Testing/MeshTests/MeshTests.cpp +++ b/Testing/MeshTests/MeshTests.cpp @@ -1182,8 +1182,9 @@ TEST(MeshTests, nonAsciiPathTest) { } TEST(MeshTests, longPathTest) { - // Paths beyond MAX_PATH need the \\?\ prefix on Windows. The readers apply it; the existence - // check in front of them did not. (#2648) + // Paths beyond MAX_PATH need the \\?\ prefix on Windows. The STL reader applies it; the + // existence check in front of it did not. (#2648) This uses .stl deliberately: VTK's legacy .vtk + // reader stats the file without the prefix first, so it can't open such a path on Windows at all. auto dir = TestUtils::Instance().get_output_dir("long_path_test"); std::string deep = dir; @@ -1192,7 +1193,7 @@ TEST(MeshTests, longPathTest) { } ASSERT_TRUE(vtksys::SystemTools::MakeDirectory(deep)); - std::string path = deep + "/mesh.vtk"; + std::string path = deep + "/mesh.stl"; ASSERT_GT(path.size(), 260u); Mesh(std::string(TEST_DATA_DIR) + "/femur.vtk").write(path);