From 55def2ba9b97bb4e1156282ae7b2cd7bf1a7161c Mon Sep 17 00:00:00 2001 From: Vitor Sessak Date: Mon, 29 Sep 2025 15:29:40 +0000 Subject: [PATCH] Do not follow symlinks when looking for the stdlib files. This aligns MiniZinc with the standard behavior of *nix binaries that when a tool expects some directory hierarchy, that must match the reality before following any eventual symlinks, not after. In particular, the Bazel build system uses a lot of symlinks, making the behavior before this CL particularly confusing. --- lib/file_utils.cpp | 73 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/lib/file_utils.cpp b/lib/file_utils.cpp index 375e9c7dd..6fd1fd8f5 100644 --- a/lib/file_utils.cpp +++ b/lib/file_utils.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #elif defined(HAS_GETMODULEFILENAME) || defined(HAS_GETFILEATTRIBUTES) #define NOMINMAX // Ensure the words min/max remain available @@ -125,6 +126,70 @@ bool directory_exists(const std::string& dirname) { #endif } +/** + * Canonicalizes a path by resolving '..', '.', and '//' without following + * symbolic links. + * + * This function provides functionality similar to the standard POSIX + * realpath(), but it treats symbolic links as regular files/directories instead + * of resolving them. + */ +std::string realpath_nofollow(const std::string& path) { + // Splits a string by a given delimiter. + auto split = [](const std::string& s, char delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; + }; + + std::vector result_components; + + if (path.empty()) { + return working_directory(); + } + + // 1. Establish the base components (from CWD if path is relative). + if (path[0] != '/') { + result_components = split(working_directory(), '/'); + } + + // 2. Process the components of the input path, handling '.', '..', and empty + // parts. + for (const auto& part : split(path, '/')) { + if (part.empty() || part == ".") { + // Ignore empty parts (from "//" or trailing '/') and "." + continue; + } + if (part == "..") { + // Go up one level, if not already at the root + if (!result_components.empty()) { + result_components.pop_back(); + } + } else { + // Add a regular path component + result_components.push_back(part); + } + } + + // If after all processing we have no components, the path is the root + // directory. + if (result_components.empty()) { + return "/"; + } + + // 3. Rebuild the path string. + std::string current_path; + for (size_t i = 0; i < result_components.size(); ++i) { + current_path += "/" + result_components[i]; + } + + return current_path; +} + std::string file_path(const std::string& filename, const std::string& basePath) { // Add base path to relative paths if there is one auto f = !basePath.empty() && !is_absolute(filename) ? basePath + "/" + filename : filename; @@ -149,13 +214,7 @@ std::string file_path(const std::string& filename, const std::string& basePath) LocalFree(lpBuffer); return ret; #else - char* rp = realpath(f.c_str(), nullptr); - if (rp == nullptr) { - return f; - } - std::string rp_s(rp); - free(rp); - return rp_s; + return realpath_nofollow(f); #endif }