diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index ea07d8b..16a1aae 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -12,11 +12,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install clang-format 17 + - name: Install clang-format 20 run: | sudo apt-get update - sudo apt-get install -y clang-format-17 - sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-17 100 + sudo apt-get install -y clang-format-20 + sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-20 100 - name: Run format-check run: ./scripts/format-check.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 4809665..a7a1907 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ set(COMMON_SOURCES src/ArgumentParser/ArgumentManager.cpp src/ArgumentParser/ArgumentParserFactory.cpp src/App/Config.cpp + src/App/ToolConfig.cpp src/App/Files.cpp src/App/Runner.cpp src/ctrace_tools/mangle.cpp @@ -66,6 +67,9 @@ target_link_libraries(ctrace PRIVATE nlohmann_json::nlohmann_json) include(${CMAKE_SOURCE_DIR}/cmake/stackUsageAnalyzer.cmake) target_link_libraries(ctrace PRIVATE coretrace::stack_usage_analyzer_lib) +include(${CMAKE_SOURCE_DIR}/cmake/logger/coretraceLog.cmake) +target_link_libraries(ctrace PRIVATE coretrace::logger) + include(${CMAKE_SOURCE_DIR}/cmake/httpLib.cmake) target_link_libraries(ctrace PRIVATE httplib::httplib) diff --git a/cmake/logger/coretraceLog.cmake b/cmake/logger/coretraceLog.cmake new file mode 100644 index 0000000..60a470b --- /dev/null +++ b/cmake/logger/coretraceLog.cmake @@ -0,0 +1,10 @@ +set(CORETRACE_LOGGER_BUILD_EXAMPLES OFF CACHE BOOL "Disable logger examples" FORCE) +set(CORETRACE_LOGGER_BUILD_TESTS OFF CACHE BOOL "Disable logger tests" FORCE) + +include(FetchContent) + +FetchContent_Declare(coretrace-logger + GIT_REPOSITORY https://github.com/CoreTrace/coretrace-log.git + GIT_TAG main +) +FetchContent_MakeAvailable(coretrace-logger) diff --git a/config/tool-config.json b/config/tool-config.json new file mode 100644 index 0000000..d9df6ed --- /dev/null +++ b/config/tool-config.json @@ -0,0 +1,24 @@ +{ + "invoke": [ + "ctrace_stack_analyzer" + ], + "stack_analyzer": { + "compile_commands": "", + "include_compdb_deps": false, + "analysis-profile": "full", + "timing": true, + "smt": "on", + "smt-backend": "z3", + "smt-secondary-backend": "single", + "smt-mode": "single", + "smt-timeout-ms": 80, + "smt-rules": ["recursion","integer-overflow","size-minus-k","stack-buffer","oob-read","type-confusion"], + "entry_points": [], + "demangle": true, + "quiet": false, + "stack_limit": 8388608, + "resource_model": "../../coretrace-stack-analyzer/models/resource-lifetime/generic.txt", + "escape_model": "../../coretrace-stack-analyzer/models/stack-escape/generic.txt", + "buffer_model": "../../coretrace-stack-analyzer/models/buffer-overflow/generic.txt" + } +} diff --git a/include/App/ToolConfig.hpp b/include/App/ToolConfig.hpp new file mode 100644 index 0000000..5bea874 --- /dev/null +++ b/include/App/ToolConfig.hpp @@ -0,0 +1,15 @@ +#ifndef APP_TOOL_CONFIG_HPP +#define APP_TOOL_CONFIG_HPP + +#include +#include + +#include "Config/config.hpp" + +namespace ctrace +{ + bool applyToolConfigFile(ProgramConfig& config, std::string_view configPath, + std::string& errorMessage); +} + +#endif // APP_TOOL_CONFIG_HPP diff --git a/include/Config/config.hpp b/include/Config/config.hpp index 36c7988..347e909 100644 --- a/include/Config/config.hpp +++ b/include/Config/config.hpp @@ -2,6 +2,7 @@ #define CONFIG_HPP #include +#include #include #include #include @@ -13,6 +14,8 @@ #include "ctrace_tools/strings.hpp" #include "ctrace_defs/types.hpp" +#include + static void printHelp(void) { std::cout << R"(ctrace - Static & Dynamic C/C++ Code Analysis Tool @@ -27,6 +30,21 @@ static void printHelp(void) --report-file Specifies the path to the report file (default: ctrace-report.txt). --output-file Specifies the output file for the analysed binary (default: ctrace.out). --entry-points Sets the entry points for analysis (default: main). Accepts a comma-separated list. + --config Loads settings from a JSON config file. + --compile-commands Path to compile_commands.json for tools that support it. + --include-compdb-deps Includes dependency entries (e.g. _deps) when auto-loading files from compile_commands.json. + --analysis-profile

Stack analyzer profile: fast|full. + --smt Enables/disables SMT refinement in stack analyzer. + --smt-backend Primary SMT backend (e.g. z3, interval). + --smt-secondary-backend Secondary backend for multi-solver modes. + --smt-mode SMT mode: single|portfolio|cross-check|dual-consensus. + --smt-timeout-ms SMT timeout in milliseconds. + --smt-budget-nodes SMT node budget per query. + --smt-rules Comma-separated SMT-enabled rules. + --resource-model Path to the resource lifetime model for stack analyzer. + --escape-model Path to the stack escape model for stack analyzer. + --buffer-model Path to the buffer overflow model for stack analyzer. + --demangle Displays demangled function names in supported tools. --static Enables static analysis. --dyn Enables dynamic analysis. --invoke Invokes specific tools (comma-separated). @@ -66,15 +84,23 @@ namespace ctrace explicit FileConfig(const std::string& str) : src_file(str) {} }; + struct SpecificConfig + { + std::string tool_name; ///< Name of the specific tool. + bool timing = false; ///< Indicates if timing information should be displayed. + }; + /** * @brief Represents the global configuration for the program. * * The `GlobalConfig` struct stores various global settings, such as verbosity, * analysis options, and file paths for reports and outputs. */ - struct GlobalConfig + struct GlobalConfig : public SpecificConfig { - bool verbose = false; ///< Enables verbose output. + bool verbose = false; ///< Enables verbose output. + bool quiet = false; ///< Suppresses non-essential output. + bool demangle = false; ///< Enables demangled function names in supported tools. std::launch hasAsync = std::launch::deferred; ///< Enables asynchronous execution. bool hasSarifFormat = false; ///< Indicates if SARIF format is enabled. bool hasStaticAnalysis = false; ///< Indicates if static analysis is enabled. @@ -90,9 +116,25 @@ namespace ctrace std::vector specificTools; ///< List of specific tools to invoke. - std::string entry_points = "main"; ///< Entry points for analysis. + std::string entry_points = ""; ///< Entry points for analysis. std::string report_file = "ctrace-report.txt"; ///< Path to the report file. std::string output_file = "ctrace.out"; ///< Path to the output file. + std::string config_file; ///< Path to the JSON config file. + std::string compile_commands; ///< Path to compile_commands.json. + bool include_compdb_deps = + false; ///< Includes dependency entries from compile_commands.json auto-discovery. + std::string analysis_profile; ///< Stack analyzer analysis profile (fast|full). + std::string smt; ///< SMT enable switch (on|off). + std::string smt_backend; ///< SMT primary backend. + std::string smt_secondary_backend; ///< SMT secondary backend. + std::string smt_mode; ///< SMT mode. + uint32_t smt_timeout_ms = 0; ///< SMT timeout in milliseconds (0 = analyzer default). + uint64_t smt_budget_nodes = 0; ///< SMT node budget (0 = analyzer default). + std::vector smt_rules; ///< SMT-enabled rule ids. + std::string resource_model; ///< Path to stack analyzer resource model. + std::string escape_model; ///< Path to stack analyzer escape model. + std::string buffer_model; ///< Path to stack analyzer buffer model. + uint64_t stack_limit = 8 * 1024 * 1024; ///< Stack limit in bytes. }; /** @@ -148,6 +190,8 @@ namespace ctrace std::exit(0); }; commands["--verbose"] = [this](const std::string&) { config.global.verbose = true; }; + commands["--quiet"] = [this](const std::string&) { config.global.quiet = true; }; + commands["--demangle"] = [this](const std::string&) { config.global.demangle = true; }; commands["--sarif-format"] = [this](const std::string&) { config.global.hasSarifFormat = true; }; commands["--report-file"] = [this](const std::string& value) @@ -199,6 +243,78 @@ namespace ctrace { config.global.hasDynamicAnalysis = true; }; commands["--entry-points"] = [this](const std::string& value) { config.global.entry_points = value; }; + commands["--config"] = [this](const std::string& value) + { config.global.config_file = value; }; + commands["--compile-commands"] = [this](const std::string& value) + { config.global.compile_commands = value; }; + commands["--include-compdb-deps"] = [this](const std::string&) + { config.global.include_compdb_deps = true; }; + commands["--analysis-profile"] = [this](const std::string& value) + { config.global.analysis_profile = value; }; + commands["--smt"] = [this](const std::string& value) { config.global.smt = value; }; + commands["--smt-backend"] = [this](const std::string& value) + { config.global.smt_backend = value; }; + commands["--smt-secondary-backend"] = [this](const std::string& value) + { config.global.smt_secondary_backend = value; }; + commands["--smt-mode"] = [this](const std::string& value) + { config.global.smt_mode = value; }; + commands["--smt-timeout-ms"] = [this](const std::string& value) + { + try + { + config.global.smt_timeout_ms = static_cast(std::stoul(value)); + } + catch (const std::exception& e) + { + coretrace::log(coretrace::Level::Error, + "Invalid smt timeout value: '{}'. Error: {}\n", value, e.what()); + std::exit(EXIT_FAILURE); + } + }; + commands["--smt-budget-nodes"] = [this](const std::string& value) + { + try + { + config.global.smt_budget_nodes = std::stoull(value); + } + catch (const std::exception& e) + { + coretrace::log(coretrace::Level::Error, + "Invalid smt budget value: '{}'. Error: {}\n", value, e.what()); + std::exit(EXIT_FAILURE); + } + }; + commands["--smt-rules"] = [this](const std::string& value) + { + config.global.smt_rules.clear(); + for (const auto rule : ctrace_tools::strings::splitByComma(value)) + { + config.global.smt_rules.emplace_back(rule); + } + }; + commands["--resource-model"] = [this](const std::string& value) + { config.global.resource_model = value; }; + commands["--escape-model"] = [this](const std::string& value) + { config.global.escape_model = value; }; + commands["--buffer-model"] = [this](const std::string& value) + { config.global.buffer_model = value; }; + commands["--stack-limit"] = [this](const std::string& value) + { + try + { + config.global.stack_limit = std::stoul(value); + coretrace::log(coretrace::Level::Info, "Stack limit set to {} bytes", + config.global.stack_limit); + } + catch (const std::exception& e) + { + coretrace::log(coretrace::Level::Error, + "Invalid stack limit value: '{}'. Error: {}\n", value, e.what()); + coretrace::log(coretrace::Level::Error, + "Please provide a valid unsigned integer.\n"); + std::exit(EXIT_FAILURE); + } + }; commands["--ipc"] = [this](const std::string& value) { auto ipc_list = ctrace_defs::IPC_TYPES; @@ -223,12 +339,14 @@ namespace ctrace commands["--serve-host"] = [this](const std::string& value) { config.global.serverHost = value; - std::cout << "[DEBUG] Server host set to " << config.global.serverHost << std::endl; + coretrace::log(coretrace::Level::Debug, "Server host set to {}", + config.global.serverHost); }; commands["--serve-port"] = [this](const std::string& value) { config.global.serverPort = std::stoi(value); - std::cout << "[DEBUG] Server port set to " << config.global.serverPort << std::endl; + coretrace::log(coretrace::Level::Debug, "Server port set to {}", + config.global.serverPort); }; commands["--shutdown-token"] = [this](const std::string& value) { config.global.shutdownToken = value; }; diff --git a/include/Process/Ipc/HttpServer.hpp b/include/Process/Ipc/HttpServer.hpp index 5e999ee..7b8eda6 100644 --- a/include/Process/Ipc/HttpServer.hpp +++ b/include/Process/Ipc/HttpServer.hpp @@ -15,8 +15,11 @@ #include // nlohmann::json (header-only) #include "Config/config.hpp" +#include "App/Files.hpp" +#include "App/ToolConfig.hpp" #include "Process/Tools/ToolsInvoker.hpp" #include "ctrace_tools/strings.hpp" +#include "coretrace/logger.hpp" using json = nlohmann::json; @@ -37,22 +40,22 @@ class ConsoleLogger : public ILogger public: void info(const std::string& msg) override { - std::cout << "[INFO] :: " << msg << '\n'; + coretrace::log(coretrace::Level::Info, msg); } void error(const std::string& msg) override { - std::cerr << "[ERROR] :: " << msg << '\n'; + coretrace::log(coretrace::Level::Error, msg); } void debug(const std::string& msg) { - std::cout << "[DEBUG] :: " << msg << '\n'; + coretrace::log(coretrace::Level::Debug, msg); } void warn(const std::string& msg) { - std::cout << "[WARN] :: " << msg << '\n'; + coretrace::log(coretrace::Level::Warn, msg); } }; @@ -310,6 +313,7 @@ class ApiHandler {"sarif_format", &config.global.hasSarifFormat}, {"static_analysis", &config.global.hasStaticAnalysis}, {"dynamic_analysis", &config.global.hasDynamicAnalysis}, + {"include_compdb_deps", &config.global.include_compdb_deps}, })) { return false; @@ -318,12 +322,38 @@ class ApiHandler { return false; } - if (!apply_string_fields(params, err, - { - {"report_file", &config.global.report_file}, - {"output_file", &config.global.output_file}, - {"ipc_path", &config.global.ipcPath}, - })) + std::string configPath; + if (!read_string(params, "config", configPath, err)) + { + return false; + } + if (!configPath.empty()) + { + std::string toolConfigError; + if (!ctrace::applyToolConfigFile(config, configPath, toolConfigError)) + { + err = {"InvalidParams", "Failed to load config: " + toolConfigError}; + return false; + } + config.global.config_file = configPath; + } + if (!apply_string_fields( + params, err, + { + {"report_file", &config.global.report_file}, + {"output_file", &config.global.output_file}, + {"config", &config.global.config_file}, + {"compile_commands", &config.global.compile_commands}, + {"analysis_profile", &config.global.analysis_profile}, + {"smt", &config.global.smt}, + {"smt_backend", &config.global.smt_backend}, + {"smt_secondary_backend", &config.global.smt_secondary_backend}, + {"smt_mode", &config.global.smt_mode}, + {"resource_model", &config.global.resource_model}, + {"escape_model", &config.global.escape_model}, + {"buffer_model", &config.global.buffer_model}, + {"ipc_path", &config.global.ipcPath}, + })) { return false; } @@ -342,6 +372,11 @@ class ApiHandler { return false; } + if (!apply_list_param(params, "smt_rules", err, [&](const std::vector& values) + { config.global.smt_rules = values; })) + { + return false; + } if (!apply_list_param(params, "input", err, [&](const std::vector& values) { @@ -367,11 +402,6 @@ class ApiHandler static bool run_analysis(const ctrace::ProgramConfig& config, ILogger& logger, json& result, ParseError& err) { - if (config.files.empty()) - { - err = {"MissingInput", "Input files are required for analysis."}; - return false; - } if (!config.global.hasStaticAnalysis && !config.global.hasDynamicAnalysis && !config.global.hasInvokedSpecificTools) { @@ -392,32 +422,51 @@ class ApiHandler const uint8_t pool_size = static_cast(threads); auto output_capture = std::make_shared(); ctrace::ToolInvoker invoker(config, pool_size, config.global.hasAsync, output_capture); + const auto sourceFiles = ctrace::resolveSourceFiles(config); + + if (config.global.verbose) + { + if (!config.global.config_file.empty()) + { + logger.info("Config file in use: " + config.global.config_file); + } + else + { + logger.info("Config file in use: none (request values only)"); + } + } - size_t processed = 0; - for (const auto& file : config.files) + std::vector validSourceFiles; + validSourceFiles.reserve(sourceFiles.size()); + for (const auto& file : sourceFiles) { - if (file.src_file.empty()) + if (!file.empty()) { - continue; + validSourceFiles.push_back(file); } - ++processed; + } + + const size_t processed = validSourceFiles.size(); + if (processed > 0) + { if (config.global.hasStaticAnalysis) { - invoker.runStaticTools(file.src_file); + invoker.runStaticTools(validSourceFiles); } if (config.global.hasDynamicAnalysis) { - invoker.runDynamicTools(file.src_file); + invoker.runDynamicTools(validSourceFiles); } if (config.global.hasInvokedSpecificTools) { - invoker.runSpecificTools(config.global.specificTools, file.src_file); + invoker.runSpecificTools(config.global.specificTools, validSourceFiles); } } if (processed == 0) { - err = {"MissingInput", "Input files are required for analysis."}; + err = {"MissingInput", + "Input files are required for analysis (or provide --compile-commands)."}; return false; } @@ -429,6 +478,17 @@ class ApiHandler result["invoked_tools"] = config.global.specificTools; result["sarif_format"] = config.global.hasSarifFormat; result["report_file"] = config.global.report_file; + result["config"] = config.global.config_file; + result["include_compdb_deps"] = config.global.include_compdb_deps; + result["resource_model"] = config.global.resource_model; + result["escape_model"] = config.global.escape_model; + result["buffer_model"] = config.global.buffer_model; + result["analysis_profile"] = config.global.analysis_profile; + result["smt"] = config.global.smt; + result["smt_backend"] = config.global.smt_backend; + result["smt_secondary_backend"] = config.global.smt_secondary_backend; + result["smt_mode"] = config.global.smt_mode; + result["smt_rules"] = config.global.smt_rules; if (output_capture) { json outputs = json::object(); @@ -556,7 +616,7 @@ class HttpServer handle_post_shutdown(req, res); }); - logger_.info("[SERVER] Listening on http://" + host + ":" + std::to_string(port)); + logger_.info("Listening on http://" + host + ":" + std::to_string(port)); server_.listen(host.c_str(), port); finalize_shutdown(); } diff --git a/include/Process/Tools/AnalysisTools.hpp b/include/Process/Tools/AnalysisTools.hpp index c4cc222..1da3c9d 100644 --- a/include/Process/Tools/AnalysisTools.hpp +++ b/include/Process/Tools/AnalysisTools.hpp @@ -153,7 +153,17 @@ namespace ctrace { public: void execute(const std::string& file, ctrace::ProgramConfig config) const override; + [[nodiscard]] bool supportsBatchExecution() const override + { + return true; + } + void executeBatch(const std::vector& files, + ctrace::ProgramConfig config) const override; + [[nodiscard]] DiagnosticSummary lastDiagnosticsSummary() const override; std::string name() const override; + + private: + mutable DiagnosticSummary m_lastDiagnosticsSummary{}; }; class FlawfinderToolImplementation : public AnalysisToolBase diff --git a/include/Process/Tools/IAnalysisTools.hpp b/include/Process/Tools/IAnalysisTools.hpp index 54ed634..dafbf63 100644 --- a/include/Process/Tools/IAnalysisTools.hpp +++ b/include/Process/Tools/IAnalysisTools.hpp @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include "Config/config.hpp" @@ -11,6 +13,12 @@ class IpcStrategy; namespace ctrace { + struct DiagnosticSummary + { + std::size_t info = 0; + std::size_t warning = 0; + std::size_t error = 0; + }; /** * @brief Interface for an analysis tool (Strategy pattern). @@ -40,6 +48,47 @@ namespace ctrace */ virtual void execute(const std::string& file, ctrace::ProgramConfig config) const = 0; + /** + * @brief Indicates whether the tool can process multiple inputs in one run. + * + * Tools returning true will be scheduled once with the full resolved input list. + * Default behavior keeps per-file execution. + */ + [[nodiscard]] virtual bool supportsBatchExecution() const + { + return false; + } + + /** + * @brief Executes the analysis tool on multiple files in one run. + * + * Default implementation falls back to per-file execution. + * + * @param files The list of files to analyze. + * @param config The program configuration to use during the analysis. + */ + virtual void executeBatch(const std::vector& files, + ctrace::ProgramConfig config) const + { + for (const auto& file : files) + { + execute(file, config); + } + } + + /** + * @brief Returns the last diagnostics summary produced by the tool. + * + * Tools that do not expose structured diagnostics can keep the default + * implementation returning a zeroed summary. + * + * @return DiagnosticSummary Counts grouped by severity. + */ + [[nodiscard]] virtual DiagnosticSummary lastDiagnosticsSummary() const + { + return {}; + } + /** * @brief Retrieves the name of the analysis tool. * diff --git a/include/Process/Tools/ToolsInvoker.hpp b/include/Process/Tools/ToolsInvoker.hpp index 857eb06..49f968e 100644 --- a/include/Process/Tools/ToolsInvoker.hpp +++ b/include/Process/Tools/ToolsInvoker.hpp @@ -1,29 +1,43 @@ #ifndef TOOLS_INVOKER_HPP #define TOOLS_INVOKER_HPP -#include -#include -#include -#include -#include - -#include "Process/Ipc/IpcStrategy.hpp" #include "AnalysisTools.hpp" +#include "Process/Ipc/IpcStrategy.hpp" -#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include + +#if __has_include() +#include +#endif class ThreadPool { public: - ThreadPool(size_t numThreads) + explicit ThreadPool(std::size_t numThreads) : stopping(false) { + if (numThreads == 0) + { + numThreads = 1; + } + + workers.reserve(numThreads); for (size_t i = 0; i < numThreads; ++i) { workers.emplace_back( @@ -34,8 +48,8 @@ class ThreadPool std::function task; { std::unique_lock lock(queueMutex); - condition.wait(lock, [this] { return !tasks.empty() || stop; }); - if (stop && tasks.empty()) + condition.wait(lock, [this] { return stopping || !tasks.empty(); }); + if (stopping && tasks.empty()) return; task = std::move(tasks.front()); tasks.pop(); @@ -49,26 +63,28 @@ class ThreadPool ~ThreadPool() { { - std::unique_lock lock(queueMutex); - stop = true; + std::lock_guard lock(queueMutex); + stopping = true; } condition.notify_all(); for (auto& worker : workers) { - worker.join(); + if (worker.joinable()) + { + worker.join(); + } } } - template - auto enqueue(F&& f, Args&&... args) -> std::future + template auto enqueue(F&& f) -> std::future>> { - using return_type = decltype(f(args...)); - auto task = std::make_shared>( - std::bind(std::forward(f), std::forward(args)...)); + using return_type = std::invoke_result_t>; + auto task = std::make_shared>(std::forward(f)); + std::future res = task->get_future(); { - std::unique_lock lock(queueMutex); - if (stop) + std::lock_guard lock(queueMutex); + if (stopping) throw std::runtime_error("Enqueue on stopped ThreadPool"); tasks.emplace([task]() { (*task)(); }); } @@ -77,34 +93,39 @@ class ThreadPool } private: - std::vector workers; +#if defined(__cpp_lib_jthread) && (__cpp_lib_jthread >= 201911L) + using WorkerThread = std::jthread; +#else + using WorkerThread = std::thread; +#endif + std::vector workers; std::queue> tasks; std::mutex queueMutex; std::condition_variable condition; - bool stop = false; + bool stopping; }; namespace ctrace { - class ToolInvoker { public: - ToolInvoker(ctrace::ProgramConfig config, uint8_t nbThreadPool, std::launch policy, + ToolInvoker(ctrace::ProgramConfig config, std::size_t nbThreadPool, std::launch policy, std::shared_ptr output_capture = nullptr) - : m_config(config), m_nbThreadPool(nbThreadPool), m_policy(policy), - m_output_capture(output_capture) + : m_config(std::move(config)), m_nbThreadPool(nbThreadPool == 0 ? 1 : nbThreadPool), + m_policy(policy), m_output_capture(output_capture) { - std::cout << "\033[36mInitializing ToolInvoker...\033[0m\n"; + coretrace::log(coretrace::Level::Info, "Initializing ToolInvoker...\n"); - tools["cppcheck"] = std::make_unique(); - tools["flawfinder"] = std::make_unique(); - tools["tscancode"] = std::make_unique(); - tools["ikos"] = std::make_unique(); - tools["ctrace_stack_analyzer"] = std::make_unique(); - tools["dyn_tools_1"] = std::make_unique(); - tools["dyn_tools_2"] = std::make_unique(); - tools["dyn_tools_3"] = std::make_unique(); + registerTool("cppcheck", std::make_unique()); + registerTool("flawfinder", std::make_unique()); + registerTool("tscancode", std::make_unique()); + registerTool("ikos", std::make_unique()); + registerTool("ctrace_stack_analyzer", + std::make_unique()); + registerTool("dyn_tools_1", std::make_unique()); + registerTool("dyn_tools_2", std::make_unique()); + registerTool("dyn_tools_3", std::make_unique()); static_tools = {"cppcheck", "flawfinder", "tscancode", "ikos", "ctrace_stack_analyzer"}; dynamic_tools = {"dyn_tools_1", "dyn_tools_2", "dyn_tools_3"}; @@ -112,88 +133,284 @@ namespace ctrace if (m_config.global.ipc == "standardIO") { m_ipc = nullptr; // Use std::cout directely - std::cout << "\033[36mUsing standardIO for IPC.\033[0m\n"; + coretrace::log(coretrace::Level::Info, "Using standardIO for IPC.\n"); } else { m_ipc = std::make_shared(m_config.global.ipcPath); for (auto& [_, tool] : tools) + { tool->setIpcStrategy(m_ipc); + } + } + + if (m_policy == std::launch::async) + { + m_threadPool = std::make_unique(m_nbThreadPool); + coretrace::log(coretrace::Level::Debug, + "ToolInvoker thread pool enabled with {} workers.\n", + m_nbThreadPool); } } // Execute all static analysis tools - void runStaticTools(const std::string& file) const + void runStaticTools(const std::string& file) + { + runStaticTools(std::vector{file}); + } + + void runStaticTools(const std::vector& files) + { + runToolList(static_tools, files); + } + + // Execute all dynamic analysis tools + void runDynamicTools(const std::string& file) + { + runDynamicTools(std::vector{file}); + } + + void runDynamicTools(const std::vector& files) + { + runToolList(dynamic_tools, files); + } + + // Execute a specific tool list + void runSpecificTools(const std::vector& tool_names, const std::string& file) + { + runSpecificTools(tool_names, std::vector{file}); + } + + void runSpecificTools(const std::vector& tool_names, + const std::vector& files) + { + runToolList(deduplicateToolNames(tool_names), files); + } + + private: + void registerTool(const std::string& name, std::unique_ptr tool) + { + toolLocks[name] = std::make_shared(); + tools[name] = std::move(tool); + } + + void executeTool(const std::string& tool_name, const std::string& file) + { + ctrace::Thread::Output::CaptureContext ctx{m_output_capture, tool_name, true}; + ctrace::Thread::Output::ScopedCapture capture(m_output_capture ? &ctx : nullptr); + + auto tool_it = tools.find(tool_name); + if (tool_it == tools.end()) + { + ctrace::Thread::Output::cerr("\033[31mUnknown tool: " + tool_name + "\033[0m"); + return; + } + + auto lock_it = toolLocks.find(tool_name); + if (lock_it != toolLocks.end() && lock_it->second) + { + std::lock_guard lock(*lock_it->second); + tool_it->second->execute(file, m_config); + logDiagnosticsSummary(tool_name, *tool_it->second); + return; + } + + tool_it->second->execute(file, m_config); + logDiagnosticsSummary(tool_name, *tool_it->second); + } + + void executeBatchTool(const std::string& tool_name, const std::vector& files) { + if (files.empty()) + { + return; + } + + ctrace::Thread::Output::CaptureContext ctx{m_output_capture, tool_name, true}; + ctrace::Thread::Output::ScopedCapture capture(m_output_capture ? &ctx : nullptr); + + auto tool_it = tools.find(tool_name); + if (tool_it == tools.end()) + { + ctrace::Thread::Output::cerr("\033[31mUnknown tool: " + tool_name + "\033[0m"); + return; + } + + auto lock_it = toolLocks.find(tool_name); + if (lock_it != toolLocks.end() && lock_it->second) + { + std::lock_guard lock(*lock_it->second); + tool_it->second->executeBatch(files, m_config); + logDiagnosticsSummary(tool_name, *tool_it->second); + return; + } + + tool_it->second->executeBatch(files, m_config); + logDiagnosticsSummary(tool_name, *tool_it->second); + } + + void runToolList(const std::vector& tool_names, const std::string& file) + { + if (tool_names.empty()) + { + return; + } + + if (m_policy != std::launch::async || !m_threadPool || tool_names.size() == 1) + { + for (const auto& tool_name : tool_names) + { + executeTool(tool_name, file); + } + return; + } + + const auto containsExclusiveCaptureTool = + std::any_of(tool_names.begin(), tool_names.end(), [](const std::string& tool_name) + { return requiresExclusiveProcessCapture(tool_name); }); + + if (containsExclusiveCaptureTool) + { + std::vector> parallelResults; + parallelResults.reserve(tool_names.size()); + + for (const auto& tool_name : tool_names) + { + if (requiresExclusiveProcessCapture(tool_name)) + { + continue; + } + + parallelResults.push_back(m_threadPool->enqueue( + [this, tool_name, file] { executeTool(tool_name, file); })); + } + + for (auto& result : parallelResults) + { + result.get(); + } + + for (const auto& tool_name : tool_names) + { + if (!requiresExclusiveProcessCapture(tool_name)) + { + continue; + } + executeTool(tool_name, file); + } + return; + } + std::vector> results; - ThreadPool pool(m_nbThreadPool); - - for (const auto& tool_name : static_tools) - { - // tools.at(tool_name)->execute(file, m_config); - results.push_back(std::async(m_policy, - [this, tool_name, file]() - { - auto& tool = tools.at(tool_name); - ctrace::Thread::Output::CaptureContext ctx{ - m_output_capture, tool_name, true}; - ctrace::Thread::Output::ScopedCapture capture( - m_output_capture ? &ctx : nullptr); - return tool->execute(file, m_config); - })); + results.reserve(tool_names.size()); + + for (const auto& tool_name : tool_names) + { + results.push_back(m_threadPool->enqueue([this, tool_name, file] + { executeTool(tool_name, file); })); } + for (auto& result : results) { result.get(); } } - // Execute all dynamic analysis tools - void runDynamicTools(const std::string& file) const + void runToolList(const std::vector& tool_names, + const std::vector& files) { - for (const auto& tool_name : dynamic_tools) + if (tool_names.empty() || files.empty()) { - auto& tool = tools.at(tool_name); - ctrace::Thread::Output::CaptureContext ctx{m_output_capture, tool_name, true}; - ctrace::Thread::Output::ScopedCapture capture(m_output_capture ? &ctx : nullptr); - tool->execute(file, m_config); + return; } - } - // Execute a specific tool list - void runSpecificTools(const std::vector& tool_names, - const std::string& file) const - { - for (const auto& name : tool_names) + std::vector perFileTools; + std::vector batchTools; + std::vector unknownTools; + + perFileTools.reserve(tool_names.size()); + batchTools.reserve(tool_names.size()); + unknownTools.reserve(tool_names.size()); + + for (const auto& tool_name : tool_names) { - if (tools.count(name)) + const auto tool_it = tools.find(tool_name); + if (tool_it == tools.end()) + { + unknownTools.push_back(tool_name); + continue; + } + + if (tool_it->second->supportsBatchExecution()) { - auto& tool = tools.at(name); - ctrace::Thread::Output::CaptureContext ctx{m_output_capture, name, true}; - ctrace::Thread::Output::ScopedCapture capture(m_output_capture ? &ctx - : nullptr); - tool->execute(file, m_config); + batchTools.push_back(tool_name); } else { - ctrace::Thread::Output::CaptureContext ctx{m_output_capture, name, true}; - ctrace::Thread::Output::ScopedCapture capture(m_output_capture ? &ctx - : nullptr); - ctrace::Thread::Output::cerr("\033[31mUnknown tool: " + name + "\033[0m"); + perFileTools.push_back(tool_name); } } + + for (const auto& tool_name : unknownTools) + { + ctrace::Thread::Output::cerr("\033[31mUnknown tool: " + tool_name + "\033[0m"); + } + + for (const auto& file : files) + { + runToolList(perFileTools, file); + } + + for (const auto& tool_name : batchTools) + { + executeBatchTool(tool_name, files); + } + } + + static std::vector + deduplicateToolNames(const std::vector& tool_names) + { + std::vector deduped; + deduped.reserve(tool_names.size()); + + std::unordered_set seen; + seen.reserve(tool_names.size()); + + for (const auto& tool_name : tool_names) + { + if (seen.insert(tool_name).second) + { + deduped.push_back(tool_name); + } + } + + return deduped; + } + + static bool requiresExclusiveProcessCapture(const std::string& tool_name) + { + return tool_name == "ctrace_stack_analyzer"; + } + + static void logDiagnosticsSummary(const std::string& tool_name, const IAnalysisTool& tool) + { + const auto summary = tool.lastDiagnosticsSummary(); + coretrace::log(coretrace::Level::Info, coretrace::Module(tool_name), + "Diagnostics summary: info={}, warning={}, error={}\n", summary.info, + summary.warning, summary.error); } - private: std::unordered_map> tools; + std::unordered_map> toolLocks; std::vector static_tools; std::vector dynamic_tools; ctrace::ProgramConfig m_config; - uint8_t m_nbThreadPool; + std::size_t m_nbThreadPool; std::launch m_policy; std::shared_ptr m_ipc; std::shared_ptr m_output_capture; + std::unique_ptr m_threadPool; }; } // namespace ctrace diff --git a/main.cpp b/main.cpp index 5e8b1c6..196b0d6 100644 --- a/main.cpp +++ b/main.cpp @@ -1,18 +1,25 @@ #include "App/Config.hpp" #include "App/Runner.hpp" -#include "ctrace_tools/colors.hpp" -#include +#include int main(int argc, char* argv[]) { ctrace::ProgramConfig config = ctrace::buildConfig(argc, argv); - std::cout << ctrace::Color::GREEN << "CoreTrace - Comprehensive Tracing and Analysis Tool" - << ctrace::Color::RESET << std::endl; + // std::cout << ctrace::Color::GREEN << "CoreTrace - Comprehensive Tracing and Analysis Tool" + // << ctrace::Color::RESET << std::endl; + + coretrace::enable_logging(); + coretrace::set_prefix("== CoreTrace =="); + coretrace::set_min_level((config.global.verbose) ? coretrace::Level::Debug + : coretrace::Level::Info); + coretrace::set_source_location(false); + coretrace::set_thread_safe(false); if (config.global.ipc == "serve") { + coretrace::set_timestamps(true); return ctrace::run_server(config); } return ctrace::run_cli_analysis(config); diff --git a/scripts/format-check.sh b/scripts/format-check.sh index 31cf60e..6d020c3 100755 --- a/scripts/format-check.sh +++ b/scripts/format-check.sh @@ -4,27 +4,65 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +# Collect only first-party source files from this repository. files=() -while IFS= read -r -d '' file; do - files+=("$file") -done < <(find "${REPO_ROOT}" \ - \( -path "${REPO_ROOT}/build" -o -path "${REPO_ROOT}/external" -o -path "${REPO_ROOT}/cmake" -o -path "${REPO_ROOT}/.git" \) -prune -o \ - -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hh' -o -name '*.hpp' -o -name '*.hxx' \) -print0) +first_party_paths=( + "${REPO_ROOT}/include" + "${REPO_ROOT}/src" + "${REPO_ROOT}/tests" + "${REPO_ROOT}/main.cpp" +) +for path in "${first_party_paths[@]}"; do + if [ -d "${path}" ]; then + while IFS= read -r -d '' file; do + files+=("${file}") + done < <(find "${path}" -type f \ + \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hh' -o -name '*.hpp' -o -name '*.hxx' \) -print0) + elif [ -f "${path}" ]; then + case "${path}" in + *.c|*.cc|*.cpp|*.cxx|*.h|*.hh|*.hpp|*.hxx) + files+=("${path}") + ;; + esac + fi +done if [ "${#files[@]}" -eq 0 ]; then echo "No source files to check." exit 0 fi -echo "Checking formatting on ${#files[@]} files..." -failed=0 -for file in "${files[@]}"; do - if ! clang-format --dry-run --Werror "${file}"; then - failed=1 +if [ -n "${CLANG_FORMAT:-}" ]; then + CF_BIN="${CLANG_FORMAT}" +elif command -v clang-format-20 >/dev/null 2>&1; then + CF_BIN="clang-format-20" +elif command -v clang-format >/dev/null 2>&1; then + CF_BIN="clang-format" +else + CF_BIN="clang-format" +fi + +if ! command -v "${CF_BIN}" >/dev/null 2>&1; then + echo "clang-format binary not found: ${CF_BIN}" + echo "Set CLANG_FORMAT or install clang-format." + exit 1 +fi + +if [ "${CF_BIN}" = "clang-format" ]; then + version_line="$(${CF_BIN} --version 2>/dev/null || true)" + if [[ "${version_line}" =~ ([0-9]+)\. ]]; then + major="${BASH_REMATCH[1]}" + if [ "${major}" -lt 20 ]; then + echo "clang-format version too old (${version_line}). Need >= 20." + echo "Set CLANG_FORMAT to clang-format-20 or install a newer clang-format." + exit 1 + fi fi -done +fi -if [ "${failed}" -ne 0 ]; then +echo "Using ${CF_BIN}." +echo "Checking formatting on ${#files[@]} first-party files..." +if ! "${CF_BIN}" --dry-run --Werror "${files[@]}"; then echo "Formatting check failed. Run scripts/format.sh to fix." exit 1 fi diff --git a/scripts/format.sh b/scripts/format.sh index 4fc02be..adbf4e1 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -4,17 +4,61 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +# Collect only first-party source files from this repository. files=() -while IFS= read -r -d '' file; do - files+=("$file") -done < <(find "${REPO_ROOT}" \ - \( -path "${REPO_ROOT}/build" -o -path "${REPO_ROOT}/external" -o -path "${REPO_ROOT}/cmake" -o -path "${REPO_ROOT}/.git" \) -prune -o \ - -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hh' -o -name '*.hpp' -o -name '*.hxx' \) -print0) +first_party_paths=( + "${REPO_ROOT}/include" + "${REPO_ROOT}/src" + "${REPO_ROOT}/tests" + "${REPO_ROOT}/main.cpp" +) +for path in "${first_party_paths[@]}"; do + if [ -d "${path}" ]; then + while IFS= read -r -d '' file; do + files+=("${file}") + done < <(find "${path}" -type f \ + \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hh' -o -name '*.hpp' -o -name '*.hxx' \) -print0) + elif [ -f "${path}" ]; then + case "${path}" in + *.c|*.cc|*.cpp|*.cxx|*.h|*.hh|*.hpp|*.hxx) + files+=("${path}") + ;; + esac + fi +done if [ "${#files[@]}" -eq 0 ]; then echo "No source files to format." exit 0 fi -echo "Formatting ${#files[@]} files with clang-format (style from ${REPO_ROOT}/.clang-format)..." -clang-format -i "${files[@]}" +if [ -n "${CLANG_FORMAT:-}" ]; then + CF_BIN="${CLANG_FORMAT}" +elif command -v clang-format-20 >/dev/null 2>&1; then + CF_BIN="clang-format-20" +elif command -v clang-format >/dev/null 2>&1; then + CF_BIN="clang-format" +else + CF_BIN="clang-format" +fi + +if ! command -v "${CF_BIN}" >/dev/null 2>&1; then + echo "clang-format binary not found: ${CF_BIN}" + echo "Set CLANG_FORMAT or install clang-format." + exit 1 +fi + +if [ "${CF_BIN}" = "clang-format" ]; then + version_line="$(${CF_BIN} --version 2>/dev/null || true)" + if [[ "${version_line}" =~ ([0-9]+)\. ]]; then + major="${BASH_REMATCH[1]}" + if [ "${major}" -lt 20 ]; then + echo "clang-format version too old (${version_line}). Need >= 20." + echo "Set CLANG_FORMAT to clang-format-20 or install a newer clang-format." + exit 1 + fi + fi +fi + +echo "Formatting ${#files[@]} first-party files with ${CF_BIN} (style from ${REPO_ROOT}/.clang-format)..." +"${CF_BIN}" -i "${files[@]}" diff --git a/src/App/Config.cpp b/src/App/Config.cpp index 7ab7158..a835c0d 100644 --- a/src/App/Config.cpp +++ b/src/App/Config.cpp @@ -1,4 +1,5 @@ #include "App/Config.hpp" +#include "App/ToolConfig.hpp" #include "ArgumentParser/ArgumentManager.hpp" #include "ArgumentParser/ArgumentParserFactory.hpp" @@ -17,6 +18,7 @@ namespace ctrace // TODO : lvl verbosity argManager.addOption("--verbose", false, 'v'); argManager.addFlag("--help", 'h'); + argManager.addFlag("--quiet", 'q'); argManager.addOption("--output", true, 'o'); argManager.addOption("--invoke", true, 'i'); argManager.addOption("--sarif-format", false, 'f'); @@ -24,6 +26,22 @@ namespace ctrace argManager.addOption("--static", false, 'x'); argManager.addOption("--dyn", false, 'd'); argManager.addOption("--entry-points", true, 'e'); + argManager.addOption("--config", true, 'j'); + argManager.addOption("--compile-commands", true, 'c'); + argManager.addOption("--include-compdb-deps", false, 'u'); + argManager.addOption("--analysis-profile", true, 'P'); + argManager.addOption("--smt", true, 'S'); + argManager.addOption("--smt-backend", true, 'N'); + argManager.addOption("--smt-secondary-backend", true, 'W'); + argManager.addOption("--smt-mode", true, 'M'); + argManager.addOption("--smt-timeout-ms", true, 'T'); + argManager.addOption("--smt-budget-nodes", true, 'G'); + argManager.addOption("--smt-rules", true, 'U'); + argManager.addOption("--resource-model", true, 'R'); + argManager.addOption("--escape-model", true, 'E'); + argManager.addOption("--buffer-model", true, 'B'); + argManager.addOption("--demangle", false, 'g'); + argManager.addOption("--stack-limit", true, 'l'); argManager.addOption("--report-file", true, 'r'); argManager.addOption("--async", false, 'a'); argManager.addOption("--ipc", true, 'p'); @@ -38,6 +56,17 @@ namespace ctrace // Traitement avec Command ConfigProcessor processor(config); + if (argManager.hasOption("--config")) + { + const auto configPath = argManager.getOptionValue("--config"); + std::string toolConfigError; + if (!ctrace::applyToolConfigFile(config, configPath, toolConfigError)) + { + std::cerr << "Error: failed to load config '" << configPath + << "': " << toolConfigError << std::endl; + std::exit(EXIT_FAILURE); + } + } processor.process(argManager); // processor.execute(argManager); diff --git a/src/App/Files.cpp b/src/App/Files.cpp index f67b586..74845f5 100644 --- a/src/App/Files.cpp +++ b/src/App/Files.cpp @@ -4,15 +4,61 @@ #include #include #include +#include +#include +#include namespace ctrace { + namespace + { + [[nodiscard]] bool hasPathSegment(const std::filesystem::path& path, + std::string_view segment) + { + if (segment.empty()) + { + return false; + } + for (const auto& part : path) + { + if (part == std::filesystem::path(segment)) + { + return true; + } + } + return false; + } + } // namespace + CT_NODISCARD std::vector resolveSourceFiles(const ProgramConfig& config) { using json = nlohmann::json; std::vector sourceFiles; - sourceFiles.reserve(config.files.size()); + std::unordered_set seenPaths; + seenPaths.reserve(config.files.size() + 1); + + auto fileEntries = config.files; + std::filesystem::path autoDiscoveredCompdbPath; + bool hasAutoDiscoveredCompdbPath = false; + if (fileEntries.empty() && !config.global.compile_commands.empty()) + { + std::filesystem::path compdbPath(config.global.compile_commands); + std::error_code fsErr; + if (std::filesystem::is_directory(compdbPath, fsErr)) + { + compdbPath /= "compile_commands.json"; + } + if (std::filesystem::is_regular_file(compdbPath, fsErr)) + { + compdbPath = compdbPath.lexically_normal(); + fileEntries.emplace_back(compdbPath.string()); + autoDiscoveredCompdbPath = compdbPath; + hasAutoDiscoveredCompdbPath = true; + } + } + + sourceFiles.reserve(fileEntries.size()); const auto appendResolved = [&](const std::string& candidate, const std::filesystem::path& baseDir) -> bool @@ -27,13 +73,39 @@ namespace ctrace resolved = baseDir / resolved; } resolved = resolved.lexically_normal(); - sourceFiles.emplace_back(resolved.string()); + const auto resolvedStr = resolved.string(); + if (!seenPaths.insert(resolvedStr).second) + { + return false; + } + sourceFiles.emplace_back(resolvedStr); return true; }; - for (const auto& fileConfig : config.files) + for (const auto& fileConfig : fileEntries) { const std::string& entry = fileConfig.src_file; + const std::filesystem::path entryPath(entry); + const bool isCompdbAutoDiscoveryEntry = + hasAutoDiscoveredCompdbPath && + (entryPath.lexically_normal() == autoDiscoveredCompdbPath); + const bool filterDependencyEntries = + isCompdbAutoDiscoveryEntry && !config.global.include_compdb_deps; + + const auto shouldSkipDependencyEntry = [&](const std::string& candidate, + const std::filesystem::path& baseDir) -> bool + { + if (!filterDependencyEntries || candidate.empty()) + { + return false; + } + std::filesystem::path resolved(candidate); + if (resolved.is_relative() && !baseDir.empty()) + { + resolved = baseDir / resolved; + } + return hasPathSegment(resolved.lexically_normal(), "_deps"); + }; bool expanded = false; if (!entry.empty() && (entry.ends_with(".json") || entry.ends_with(".JSON"))) @@ -57,28 +129,54 @@ namespace ctrace { if (item.is_string()) { - appended |= - appendResolved(item.get(), manifestDir); + const auto candidate = item.get(); + if (shouldSkipDependencyEntry(candidate, manifestDir)) + { + continue; + } + appended |= appendResolved(candidate, manifestDir); } else if (item.is_object()) { if (const auto it = item.find("file"); it != item.end() && it->is_string()) { - appended |= - appendResolved(it->get(), manifestDir); + std::filesystem::path entryBase = manifestDir; + if (const auto itDir = item.find("directory"); + itDir != item.end() && itDir->is_string()) + { + entryBase = itDir->get(); + if (entryBase.is_relative()) + { + entryBase = manifestDir / entryBase; + } + } + const auto candidate = it->get(); + if (shouldSkipDependencyEntry(candidate, entryBase)) + { + continue; + } + appended |= appendResolved(candidate, entryBase); } else if (const auto itSrc = item.find("src_file"); itSrc != item.end() && itSrc->is_string()) { - appended |= - appendResolved(itSrc->get(), manifestDir); + const auto candidate = itSrc->get(); + if (shouldSkipDependencyEntry(candidate, manifestDir)) + { + continue; + } + appended |= appendResolved(candidate, manifestDir); } else if (const auto itPath = item.find("path"); itPath != item.end() && itPath->is_string()) { - appended |= - appendResolved(itPath->get(), manifestDir); + const auto candidate = itPath->get(); + if (shouldSkipDependencyEntry(candidate, manifestDir)) + { + continue; + } + appended |= appendResolved(candidate, manifestDir); } } } @@ -128,7 +226,11 @@ namespace ctrace if (!expanded) { - sourceFiles.emplace_back(entry); + if (!entry.empty() && (entry.ends_with(".json") || entry.ends_with(".JSON"))) + { + continue; + } + (void)appendResolved(entry, {}); } } diff --git a/src/App/Runner.cpp b/src/App/Runner.cpp index b0c4f0f..21034a1 100644 --- a/src/App/Runner.cpp +++ b/src/App/Runner.cpp @@ -3,18 +3,18 @@ #include "App/Files.hpp" #include "Process/Ipc/HttpServer.hpp" #include "Process/Tools/ToolsInvoker.hpp" -#include "ctrace_tools/colors.hpp" + +#include #include -#include #include namespace ctrace { CT_NODISCARD int run_server(const ProgramConfig& config) { - std::cout << "\033[36mStarting IPC server at " << config.global.serverHost << ":" - << config.global.serverPort << "...\033[0m\n"; + coretrace::log(coretrace::Level::Info, "Starting in server at {}:{}\n", + config.global.serverHost, std::to_string(config.global.serverPort)); ConsoleLogger logger; ApiHandler apiHandler(logger); HttpServer server(apiHandler, logger, config.global); @@ -24,58 +24,72 @@ namespace ctrace CT_NODISCARD int run_cli_analysis(const ProgramConfig& config) { - ctrace::ToolInvoker invoker(config, std::thread::hardware_concurrency(), - config.global.hasAsync); - - std::cout << ctrace::Color::CYAN << "asynchronous execution: " - << (config.global.hasAsync == std::launch::async ? ctrace::Color::GREEN - : ctrace::Color::RED) - << (config.global.hasAsync == std::launch::async ? "enabled" : "disabled") - << ctrace::Color::RESET << std::endl; - - std::cout << ctrace::Color::CYAN << "verbose: " - << (config.global.verbose ? ctrace::Color::GREEN : ctrace::Color::RED) - << config.global.verbose << ctrace::Color::RESET << std::endl; - - std::cout << ctrace::Color::CYAN << "sarif format: " - << (config.global.hasSarifFormat ? ctrace::Color::GREEN : ctrace::Color::RED) - << config.global.hasSarifFormat << ctrace::Color::RESET << std::endl; - - std::cout << ctrace::Color::CYAN << "dynamic analysis: " - << (config.global.hasDynamicAnalysis ? ctrace::Color::GREEN : ctrace::Color::RED) - << config.global.hasDynamicAnalysis << ctrace::Color::RESET << std::endl; + const auto availableThreads = std::thread::hardware_concurrency(); + const auto poolSize = (availableThreads == 0) ? 1U : availableThreads; + ctrace::ToolInvoker invoker(config, poolSize, config.global.hasAsync); - std::cout << ctrace::Color::CYAN << "Report file: " << ctrace::Color::YELLOW - << config.global.report_file << ctrace::Color::RESET << std::endl; + if (config.global.hasAsync == std::launch::async) + { + coretrace::set_thread_safe(true); + coretrace::log(coretrace::Level::Info, "Asynchronous execution enabled.\n"); + } - std::cout << ctrace::Color::CYAN << "entry point: " << ctrace::Color::YELLOW - << config.global.entry_points << ctrace::Color::RESET << std::endl; + coretrace::log(coretrace::Level::Debug, "Verbose mode enabled.\n"); + coretrace::log(coretrace::Level::Debug, "Asynchronous execution: {}\n", + (config.global.hasAsync == std::launch::async ? "enabled" : "disabled")); + coretrace::log(coretrace::Level::Debug, "Verbose mode: {}\n", + (config.global.verbose ? "enabled" : "disabled")); + coretrace::log(coretrace::Level::Debug, "Static analysis: {}\n", + (config.global.hasStaticAnalysis ? "enabled" : "disabled")); + coretrace::log(coretrace::Level::Debug, "Dynamic analysis: {}\n", + (config.global.hasDynamicAnalysis ? "enabled" : "disabled")); + coretrace::log(coretrace::Level::Debug, "SARIF format: {}\n", + (config.global.hasSarifFormat ? "enabled" : "disabled")); + coretrace::log(coretrace::Level::Debug, "Report file: {}\n", config.global.report_file); + coretrace::log(coretrace::Level::Debug, "Entry points: {}\n", config.global.entry_points); + coretrace::log(coretrace::Level::Debug, "Include compile_commands deps: {}\n", + (config.global.include_compdb_deps ? "enabled" : "disabled")); + if (!config.global.config_file.empty()) + { + coretrace::log(coretrace::Level::Debug, "Config file in use: {}\n", + config.global.config_file); + } + else + { + coretrace::log(coretrace::Level::Debug, + "Config file in use: none (CLI/runtime values only)\n"); + } std::vector sourceFiles = ctrace::resolveSourceFiles(config); + if (sourceFiles.empty()) + { + coretrace::log(coretrace::Level::Error, + "No input files resolved. Provide --input or --compile-commands.\n"); + return EXIT_FAILURE; + } for (const auto& file : sourceFiles) { - std::cout << ctrace::Color::CYAN << "File: " << ctrace::Color::YELLOW << file - << ctrace::Color::RESET << std::endl; + coretrace::log(coretrace::Level::Info, "Processing file: {}\n", file); + } - if (config.global.hasStaticAnalysis) - { - std::cout << ctrace::Color::CYAN << "Running static analysis..." - << ctrace::Color::RESET << std::endl; - invoker.runStaticTools(file); - } - if (config.global.hasDynamicAnalysis) - { - std::cout << ctrace::Color::CYAN << "Running dynamic analysis..." - << ctrace::Color::RESET << std::endl; - invoker.runDynamicTools(file); - } - if (config.global.hasInvokedSpecificTools) - { - std::cout << ctrace::Color::CYAN << "Running specific tools..." - << ctrace::Color::RESET << std::endl; - invoker.runSpecificTools(config.global.specificTools, file); - } + if (config.global.hasStaticAnalysis) + { + coretrace::log(coretrace::Level::Info, "Running static analysis on {} file(s)\n", + sourceFiles.size()); + invoker.runStaticTools(sourceFiles); + } + if (config.global.hasDynamicAnalysis) + { + coretrace::log(coretrace::Level::Info, "Running dynamic analysis on {} file(s)\n", + sourceFiles.size()); + invoker.runDynamicTools(sourceFiles); + } + if (config.global.hasInvokedSpecificTools) + { + coretrace::log(coretrace::Level::Info, "Running specific tools on {} file(s)\n", + sourceFiles.size()); + invoker.runSpecificTools(config.global.specificTools, sourceFiles); } return 0; } diff --git a/src/App/ToolConfig.cpp b/src/App/ToolConfig.cpp new file mode 100644 index 0000000..4966097 --- /dev/null +++ b/src/App/ToolConfig.cpp @@ -0,0 +1,578 @@ +#include "App/ToolConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace +{ + namespace + { + using json = nlohmann::json; + + [[nodiscard]] std::filesystem::path + resolvePathFromBase(const std::filesystem::path& baseDir, std::string_view rawPath) + { + std::filesystem::path path(rawPath); + if (path.is_relative()) + { + path = baseDir / path; + } + return path.lexically_normal(); + } + + [[nodiscard]] bool loadJsonFile(const std::filesystem::path& filePath, json& out, + std::string& errorMessage) + { + std::ifstream input(filePath); + if (!input.is_open()) + { + errorMessage = "Unable to open file: " + filePath.string(); + return false; + } + + std::ostringstream buffer; + buffer << input.rdbuf(); + out = json::parse(buffer.str(), nullptr, false); + if (out.is_discarded()) + { + errorMessage = "Invalid JSON in file: " + filePath.string(); + return false; + } + if (!out.is_object()) + { + errorMessage = "Config root must be a JSON object."; + return false; + } + return true; + } + + [[nodiscard]] bool readStringValue(const json& object, const char* key, std::string& out, + std::string& errorMessage) + { + const auto it = object.find(key); + if (it == object.end() || it->is_null()) + { + return true; + } + if (!it->is_string()) + { + errorMessage = std::string("Expected string for '") + key + "'."; + return false; + } + out = it->get(); + return true; + } + + [[nodiscard]] bool readBoolValue(const json& object, const char* key, bool& out, + std::string& errorMessage) + { + const auto it = object.find(key); + if (it == object.end() || it->is_null()) + { + return true; + } + if (!it->is_boolean()) + { + errorMessage = std::string("Expected boolean for '") + key + "'."; + return false; + } + out = it->get(); + return true; + } + + [[nodiscard]] bool readUint64Value(const json& object, const char* key, uint64_t& out, + std::string& errorMessage) + { + const auto it = object.find(key); + if (it == object.end() || it->is_null()) + { + return true; + } + if (!it->is_number_unsigned()) + { + errorMessage = std::string("Expected unsigned integer for '") + key + "'."; + return false; + } + out = it->get(); + return true; + } + + [[nodiscard]] const json* findFirstValueForKeys(const json& object, + std::initializer_list keys) + { + for (const auto* key : keys) + { + if (key == nullptr) + { + continue; + } + const auto it = object.find(key); + if (it != object.end()) + { + return &(*it); + } + } + return nullptr; + } + + [[nodiscard]] bool readStringValueAny(const json& object, + std::initializer_list keys, + std::string& out, std::string& errorMessage) + { + const json* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) + { + return true; + } + if (!value->is_string()) + { + errorMessage = "Expected string value in stack_analyzer config."; + return false; + } + out = value->get(); + return true; + } + + [[nodiscard]] bool parseBoolLikeString(const std::string& raw, bool& out) + { + std::string lowered; + lowered.reserve(raw.size()); + for (const auto ch : raw) + { + lowered.push_back(static_cast(std::tolower(static_cast(ch)))); + } + + if (lowered == "true" || lowered == "on" || lowered == "1" || lowered == "yes") + { + out = true; + return true; + } + if (lowered == "false" || lowered == "off" || lowered == "0" || lowered == "no") + { + out = false; + return true; + } + return false; + } + + [[nodiscard]] bool readBoolLikeValueAny(const json& object, + std::initializer_list keys, bool& out, + std::string& errorMessage) + { + const json* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) + { + return true; + } + if (value->is_boolean()) + { + out = value->get(); + return true; + } + if (value->is_string()) + { + const auto raw = value->get(); + if (parseBoolLikeString(raw, out)) + { + return true; + } + } + errorMessage = "Expected boolean or boolean-like string in stack_analyzer config."; + return false; + } + + [[nodiscard]] bool readUint64ValueAny(const json& object, + std::initializer_list keys, + uint64_t& out, std::string& errorMessage) + { + const json* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) + { + return true; + } + if (!value->is_number_unsigned()) + { + errorMessage = "Expected unsigned integer value in stack_analyzer config."; + return false; + } + out = value->get(); + return true; + } + + [[nodiscard]] bool parseStringList(const json& value, std::vector& out, + std::string& errorMessage, const char* keyName) + { + out.clear(); + if (value.is_string()) + { + out.push_back(value.get()); + return true; + } + if (!value.is_array()) + { + errorMessage = + std::string("Expected string or array of strings for '") + keyName + "'."; + return false; + } + out.reserve(value.size()); + for (const auto& item : value) + { + if (!item.is_string()) + { + errorMessage = std::string("Expected string entries in '") + keyName + "'."; + return false; + } + out.push_back(item.get()); + } + return true; + } + + [[nodiscard]] bool readStringListValueAny(const json& object, + std::initializer_list keys, + std::vector& out, + std::string& errorMessage, const char* keyName) + { + const json* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) + { + return true; + } + return parseStringList(*value, out, errorMessage, keyName); + } + + void appendUnique(std::vector& target, const std::string& value) + { + if (value.empty()) + { + return; + } + if (std::find(target.begin(), target.end(), value) == target.end()) + { + target.push_back(value); + } + } + + [[nodiscard]] std::string joinComma(const std::vector& values) + { + std::string joined; + for (std::size_t i = 0; i < values.size(); ++i) + { + if (i != 0) + { + joined += ","; + } + joined += values[i]; + } + return joined; + } + + [[nodiscard]] const json* findStackAnalyzerConfigSection(const json& root) + { + if (const auto it = root.find("stack_analyzer"); it != root.end() && it->is_object()) + { + return &(*it); + } + if (const auto it = root.find("tools"); it != root.end() && it->is_object()) + { + if (const auto itTool = it->find("ctrace_stack_analyzer"); + itTool != it->end() && itTool->is_object()) + { + return &(*itTool); + } + if (const auto itTool = it->find("stack_analyzer"); + itTool != it->end() && itTool->is_object()) + { + return &(*itTool); + } + } + return nullptr; + } + + [[nodiscard]] bool applyInvoke(const json& root, ProgramConfig& config, + std::string& errorMessage) + { + const auto it = root.find("invoke"); + if (it == root.end() || it->is_null()) + { + return true; + } + + std::vector tools; + if (!parseStringList(*it, tools, errorMessage, "invoke")) + { + return false; + } + + if (!tools.empty()) + { + config.global.hasInvokedSpecificTools = true; + for (const auto& tool : tools) + { + appendUnique(config.global.specificTools, tool); + } + } + return true; + } + + [[nodiscard]] bool applyInputFiles(const json& root, const std::filesystem::path& configDir, + ProgramConfig& config, std::string& errorMessage) + { + const auto it = root.find("input"); + if (it == root.end() || it->is_null()) + { + return true; + } + + std::vector entries; + if (!parseStringList(*it, entries, errorMessage, "input")) + { + return false; + } + + for (const auto& entry : entries) + { + if (entry.empty()) + { + continue; + } + const auto resolved = resolvePathFromBase(configDir, entry); + config.files.emplace_back(resolved.string()); + } + return true; + } + + [[nodiscard]] bool applyStackAnalyzerConfig(const json& section, + const std::filesystem::path& configDir, + ProgramConfig& config, + std::string& errorMessage) + { + if (!section.is_object()) + { + errorMessage = "stack_analyzer config must be a JSON object."; + return false; + } + + std::string pathValue; + if (!readStringValue(section, "compile_commands", pathValue, errorMessage)) + { + return false; + } + if (!pathValue.empty()) + { + config.global.compile_commands = resolvePathFromBase(configDir, pathValue).string(); + } + + pathValue.clear(); + if (!readStringValue(section, "resource_model", pathValue, errorMessage)) + { + return false; + } + if (!pathValue.empty()) + { + config.global.resource_model = resolvePathFromBase(configDir, pathValue).string(); + } + + pathValue.clear(); + if (!readStringValue(section, "escape_model", pathValue, errorMessage)) + { + return false; + } + if (!pathValue.empty()) + { + config.global.escape_model = resolvePathFromBase(configDir, pathValue).string(); + } + + pathValue.clear(); + if (!readStringValue(section, "buffer_model", pathValue, errorMessage)) + { + return false; + } + if (!pathValue.empty()) + { + config.global.buffer_model = resolvePathFromBase(configDir, pathValue).string(); + } + + if (!readBoolValue(section, "demangle", config.global.demangle, errorMessage)) + { + return false; + } + if (!readBoolValue(section, "timing", config.global.timing, errorMessage)) + { + return false; + } + if (!readBoolValue(section, "include_compdb_deps", config.global.include_compdb_deps, + errorMessage)) + { + return false; + } + if (!readBoolValue(section, "quiet", config.global.quiet, errorMessage)) + { + return false; + } + if (!readUint64Value(section, "stack_limit", config.global.stack_limit, errorMessage)) + { + return false; + } + + std::string stringValue; + if (!readStringValueAny(section, {"analysis-profile", "analysis_profile"}, stringValue, + errorMessage)) + { + return false; + } + if (!stringValue.empty()) + { + config.global.analysis_profile = stringValue; + } + + bool boolValue = false; + if (!readBoolLikeValueAny(section, {"smt"}, boolValue, errorMessage)) + { + return false; + } + if (const auto* smtValue = findFirstValueForKeys(section, {"smt"}); + smtValue != nullptr && !smtValue->is_null()) + { + config.global.smt = boolValue ? "on" : "off"; + } + + stringValue.clear(); + if (!readStringValueAny(section, {"smt-backend", "smt_backend"}, stringValue, + errorMessage)) + { + return false; + } + if (!stringValue.empty()) + { + config.global.smt_backend = stringValue; + } + + stringValue.clear(); + if (!readStringValueAny(section, {"smt-secondary-backend", "smt_secondary_backend"}, + stringValue, errorMessage)) + { + return false; + } + if (!stringValue.empty()) + { + config.global.smt_secondary_backend = stringValue; + } + + stringValue.clear(); + if (!readStringValueAny(section, {"smt-mode", "smt_mode"}, stringValue, errorMessage)) + { + return false; + } + if (!stringValue.empty()) + { + config.global.smt_mode = stringValue; + } + + uint64_t uint64Value = 0; + if (!readUint64ValueAny(section, {"smt-timeout-ms", "smt_timeout_ms"}, uint64Value, + errorMessage)) + { + return false; + } + if (const auto* timeoutValue = + findFirstValueForKeys(section, {"smt-timeout-ms", "smt_timeout_ms"}); + timeoutValue != nullptr && !timeoutValue->is_null()) + { + if (uint64Value > std::numeric_limits::max()) + { + errorMessage = "smt-timeout-ms is too large."; + return false; + } + config.global.smt_timeout_ms = static_cast(uint64Value); + } + + uint64Value = 0; + if (!readUint64ValueAny(section, {"smt-budget-nodes", "smt_budget_nodes"}, uint64Value, + errorMessage)) + { + return false; + } + if (const auto* budgetValue = + findFirstValueForKeys(section, {"smt-budget-nodes", "smt_budget_nodes"}); + budgetValue != nullptr && !budgetValue->is_null()) + { + config.global.smt_budget_nodes = uint64Value; + } + + std::vector rules; + if (!readStringListValueAny(section, {"smt-rules", "smt_rules"}, rules, errorMessage, + "smt-rules")) + { + return false; + } + if (!rules.empty() || + (findFirstValueForKeys(section, {"smt-rules", "smt_rules"}) != nullptr)) + { + config.global.smt_rules = std::move(rules); + } + + if (const auto itEntry = section.find("entry_points"); + itEntry != section.end() && !itEntry->is_null()) + { + std::vector points; + if (!parseStringList(*itEntry, points, errorMessage, "entry_points")) + { + return false; + } + config.global.entry_points = joinComma(points); + } + + return true; + } + } // namespace + + bool applyToolConfigFile(ProgramConfig& config, std::string_view configPath, + std::string& errorMessage) + { + errorMessage.clear(); + if (configPath.empty()) + { + errorMessage = "Config path is empty."; + return false; + } + + const std::filesystem::path path(configPath); + json root; + if (!loadJsonFile(path, root, errorMessage)) + { + return false; + } + + const std::filesystem::path configDir = path.parent_path(); + config.global.config_file = path.lexically_normal().string(); + + if (!applyInvoke(root, config, errorMessage)) + { + return false; + } + if (!applyInputFiles(root, configDir, config, errorMessage)) + { + return false; + } + + if (const auto* analyzerSection = findStackAnalyzerConfigSection(root); + analyzerSection != nullptr) + { + if (!applyStackAnalyzerConfig(*analyzerSection, configDir, config, errorMessage)) + { + return false; + } + } + + return true; + } +} // namespace ctrace diff --git a/src/Process/Tools/StackAnalyzerToolImplementation.cpp b/src/Process/Tools/StackAnalyzerToolImplementation.cpp index d0f5376..2d9b309 100644 --- a/src/Process/Tools/StackAnalyzerToolImplementation.cpp +++ b/src/Process/Tools/StackAnalyzerToolImplementation.cpp @@ -1,92 +1,650 @@ #include "Process/Tools/AnalysisTools.hpp" +#include "app/AnalyzerApp.hpp" -namespace ctrace +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if !defined(_WIN32) +#include +#include +#endif + +namespace { - void StackAnalyzerToolImplementation::execute(const std::string& file, - ctrace::ProgramConfig config) const + constexpr std::string_view kStackAnalyzerModule = "stack_analyzer"; + + struct AnalyzerArgBuildResult { - ctrace::Thread::Output::cout("Running CoreTrace Stack Usage Analyzer on " + file); - std::string filename = file; + std::vector args; + std::vector bridgeReport; + }; - llvm::LLVMContext ctx; - llvm::SMDiagnostic diag; + void appendBridgeDecision(std::vector& report, std::string_view option, + bool applied, std::string_view reason) + { + std::string line; + line.reserve(option.size() + reason.size() + 24); + line.append(applied ? "[applied] " : "[skipped] "); + line.append(option); + if (!reason.empty()) + { + line.append(" - "); + line.append(reason); + } + report.emplace_back(std::move(line)); + } - ctrace::stack::AnalysisConfig cfg; - cfg.mode = ctrace::stack::AnalysisMode::IR; - cfg.stackLimit = 8 * 1024 * 1024; + [[nodiscard]] bool appendOptionValue(std::vector& args, std::string_view option, + const std::string& value) + { + if (value.empty()) + { + return false; + } + args.emplace_back(option); + args.push_back(value); + return true; + } - auto res = ctrace::stack::analyzeFile(filename, cfg, ctx, diag); + void appendFlagOption(std::vector& args, std::vector& report, + std::string_view option, bool enabled, std::string_view enabledReason, + std::string_view disabledReason) + { + if (!enabled) + { + appendBridgeDecision(report, option, false, disabledReason); + return; + } + args.emplace_back(option); + appendBridgeDecision(report, option, true, enabledReason); + } - if (config.global.ipc == "standardIO") + void appendValueOption(std::vector& args, std::vector& report, + std::string_view option, const std::string& value, + std::string_view emptyReason) + { + if (!appendOptionValue(args, option, value)) { - if (config.global.hasSarifFormat) + appendBridgeDecision(report, option, false, emptyReason); + return; + } + const std::string reason = "value='" + value + "'"; + appendBridgeDecision(report, option, true, reason); + } + + [[nodiscard]] std::string joinCsv(const std::vector& items) + { + std::string joined; + for (std::size_t i = 0; i < items.size(); ++i) + { + if (i > 0) { - ctrace::Thread::Output::tool_out(ctrace::stack::toJson(res, filename)); - return; + joined.push_back(','); } - if (res.functions.empty()) + joined += items[i]; + } + return joined; + } + + [[nodiscard]] AnalyzerArgBuildResult + buildAnalyzerArgs(const std::vector& inputFiles, + const ctrace::ProgramConfig& config) + { + AnalyzerArgBuildResult result; + std::vector& args = result.args; + std::vector& report = result.bridgeReport; + + args.reserve(inputFiles.size() + 32); + report.reserve(32); + + args.emplace_back("--mode=ir"); + appendBridgeDecision(report, "--mode=ir", true, "forced by coretrace integration"); + if (!config.global.config_file.empty()) + { + appendBridgeDecision(report, "config source", true, + "path='" + config.global.config_file + + "' loaded by coretrace and mapped to analyzer options"); + } + else + { + appendBridgeDecision(report, "config source", false, + "no --config provided to coretrace"); + } + + appendFlagOption(args, report, "--format=json", config.global.hasSarifFormat, + "coretrace --sarif-format enabled", "coretrace --sarif-format disabled"); + appendFlagOption(args, report, "--verbose", config.global.verbose, + "coretrace --verbose enabled", "coretrace --verbose disabled"); + appendFlagOption(args, report, "--demangle", config.global.demangle, + "coretrace --demangle enabled", "coretrace --demangle disabled"); + appendFlagOption(args, report, "--quiet", config.global.quiet, "coretrace --quiet enabled", + "coretrace --quiet disabled"); + appendFlagOption(args, report, "--include-compdb-deps", config.global.include_compdb_deps, + "coretrace --include-compdb-deps enabled", + "coretrace --include-compdb-deps disabled"); + + appendValueOption(args, report, "--analysis-profile", config.global.analysis_profile, + "empty in coretrace config; analyzer default kept"); + appendValueOption(args, report, "--compile-commands", config.global.compile_commands, + "empty in coretrace config; analyzer auto-discovery kept"); + appendValueOption(args, report, "--only-function", config.global.entry_points, + "empty in coretrace config; no entry-point filter"); + appendValueOption(args, report, "--resource-model", config.global.resource_model, + "empty in coretrace config; analyzer default model"); + appendValueOption(args, report, "--escape-model", config.global.escape_model, + "empty in coretrace config; analyzer default model"); + appendValueOption(args, report, "--buffer-model", config.global.buffer_model, + "empty in coretrace config; analyzer default model"); + appendValueOption(args, report, "--smt", config.global.smt, + "empty in coretrace config; analyzer default SMT"); + appendValueOption(args, report, "--smt-backend", config.global.smt_backend, + "empty in coretrace config; analyzer default backend"); + appendValueOption(args, report, "--smt-secondary-backend", + config.global.smt_secondary_backend, + "empty in coretrace config; analyzer default secondary backend"); + appendValueOption(args, report, "--smt-mode", config.global.smt_mode, + "empty in coretrace config; analyzer default mode"); + + appendFlagOption(args, report, "--timing", config.global.timing, "coretrace timing enabled", + "coretrace timing disabled"); + if (config.global.stack_limit > 0) + { + args.emplace_back("--stack-limit"); + args.emplace_back(std::to_string(config.global.stack_limit)); + appendBridgeDecision(report, "--stack-limit", true, + "value='" + std::to_string(config.global.stack_limit) + "'"); + } + else + { + appendBridgeDecision(report, "--stack-limit", false, + "value is 0; analyzer default kept"); + } + if (config.global.smt_timeout_ms > 0) + { + args.emplace_back("--smt-timeout-ms"); + args.emplace_back(std::to_string(config.global.smt_timeout_ms)); + appendBridgeDecision(report, "--smt-timeout-ms", true, + "value='" + std::to_string(config.global.smt_timeout_ms) + "'"); + } + else + { + appendBridgeDecision(report, "--smt-timeout-ms", false, + "value is 0; analyzer default kept"); + } + if (config.global.smt_budget_nodes > 0) + { + args.emplace_back("--smt-budget-nodes"); + args.emplace_back(std::to_string(config.global.smt_budget_nodes)); + appendBridgeDecision(report, "--smt-budget-nodes", true, + "value='" + std::to_string(config.global.smt_budget_nodes) + "'"); + } + else + { + appendBridgeDecision(report, "--smt-budget-nodes", false, + "value is 0; analyzer default kept"); + } + if (!config.global.smt_rules.empty()) + { + args.emplace_back("--smt-rules"); + args.emplace_back(joinCsv(config.global.smt_rules)); + appendBridgeDecision(report, "--smt-rules", true, + "value='" + joinCsv(config.global.smt_rules) + "'"); + } + else + { + appendBridgeDecision(report, "--smt-rules", false, + "empty in coretrace config; analyzer default rules"); + } + + for (const auto& file : inputFiles) + { + args.push_back(file); + } + appendBridgeDecision(report, "input files", true, + "resolved_count=" + std::to_string(inputFiles.size())); + appendBridgeDecision( + report, "coretrace-only options", false, + "--report-file/--output-file/--ipc* are handled by coretrace, not forwarded"); + + return result; + } + + [[nodiscard]] std::optional + parseDiagnosticsSummaryFromText(std::string_view text) + { + static const std::regex kTotalSummaryPattern( + R"(Total diagnostics summary:\s*info=(\d+),\s*warning=(\d+),\s*error=(\d+))"); + static const std::regex kSummaryPattern( + R"(Diagnostics summary:\s*info=(\d+),\s*warning=(\d+),\s*error=(\d+))"); + + std::string captured(text); + auto parseSummary = [](const std::smatch& match) -> std::optional + { + ctrace::DiagnosticSummary current{}; + try { - // std::err.print("stack_usage_analyzer", llvm::errs()); - return; + current.info = static_cast(std::stoull(match[1].str())); + current.warning = static_cast(std::stoull(match[2].str())); + current.error = static_cast(std::stoull(match[3].str())); } - llvm::outs() << "Mode: " - << (res.config.mode == ctrace::stack::AnalysisMode::IR ? "IR" : "ABI") - << "\n\n"; + catch (const std::exception&) + { + return std::nullopt; + } + return current; + }; - for (const auto& f : res.functions) + std::optional totalSummary; + for (std::sregex_iterator it(captured.begin(), captured.end(), kTotalSummaryPattern), end; + it != end; ++it) + { + if (const auto parsed = parseSummary(*it); parsed.has_value()) { - std::vector param_types; - // param_types.reserve(issue.inst->getFunction()->arg_size()); - param_types.push_back( - "void"); // dummy to avoid empty vector issue // refaire avec les paramèters réels + totalSummary = parsed; + } + } + if (totalSummary.has_value()) + { + return totalSummary; + } - // llvm::outs() << "Function: " << f.name << " " << ((ctrace::stack::isMangled(f.name)) ? ctrace::stack::demangle(f.name.c_str()) : "") << "\n"; - llvm::outs() << "Function: " << f.name << " " << "\n"; - llvm::outs() << " local stack: " << f.localStack << " bytes\n"; - llvm::outs() << " max stack (including callees): " << f.maxStack << " bytes\n"; + std::optional summary; + for (std::sregex_iterator it(captured.begin(), captured.end(), kSummaryPattern), end; + it != end; ++it) + { + const auto parsed = parseSummary(*it); + if (!parsed.has_value()) + { + continue; + } + if (!summary.has_value()) + { + summary = *parsed; + continue; + } - if (f.isRecursive) - llvm::outs() << " [!] recursive or mutually recursive function detected\n"; + summary->info += parsed->info; + summary->warning += parsed->warning; + summary->error += parsed->error; + } + + return summary; + } + + void captureToolOutputOnly(const std::string& stream, const std::string& message) + { + const auto* ctx = ctrace::Thread::Output::capture_context; + if (!ctx || !ctx->buffer || message.empty()) + { + return; + } + ctx->buffer->append(ctx->tool, stream, message); + } - if (f.hasInfiniteSelfRecursion) + void logMultiline(coretrace::Level level, std::string_view module, std::string_view message, + std::string_view prefix = {}) + { + std::size_t start = 0; + while (start <= message.size()) + { + const auto end = message.find('\n', start); + std::string_view line = (end == std::string_view::npos) + ? message.substr(start) + : message.substr(start, end - start); + + if (!line.empty() && line.back() == '\r') + { + line.remove_suffix(1); + } + + if (!line.empty()) + { + if (prefix.empty()) { - llvm::outs() - << " [!!!] unconditional self recursion detected (no base case)\n"; - llvm::outs() << " this will eventually overflow the stack at runtime\n"; + coretrace::log(level, coretrace::Module(module), "{}\n", line); } - - if (f.exceedsLimit) + else { - llvm::outs() << " [!] potential stack overflow: exceeds limit of " - << res.config.stackLimit << " bytes\n"; + coretrace::log(level, coretrace::Module(module), "{}{}\n", prefix, line); } + } + + if (end == std::string_view::npos) + { + break; + } + start = end + 1; + } + } + +#if !defined(_WIN32) + struct CapturedStreams + { + std::string stdoutText; + std::string stderrText; + }; + + [[nodiscard]] std::string readFdFully(int fd) + { + if (fd < 0) + { + return {}; + } + + if (lseek(fd, 0, SEEK_SET) == -1) + { + return {}; + } + + std::string content; + std::array buffer{}; + for (;;) + { + const auto bytes = read(fd, buffer.data(), buffer.size()); + if (bytes <= 0) + { + break; + } + content.append(buffer.data(), static_cast(bytes)); + } + return content; + } - if (!res.config.quiet) + class ScopedFdCapture + { + public: + ScopedFdCapture() + { + enabled_ = initialize(); + } + + ~ScopedFdCapture() + { + if (!released_) + { + (void)release(); + } + } + + [[nodiscard]] bool enabled() const + { + return enabled_; + } + + CapturedStreams release() + { + if (released_) + { + return {}; + } + + flushStreams(); + + CapturedStreams captured{ + readFdFully(capturedStdoutFd_), + readFdFully(capturedStderrFd_), + }; + + restoreDescriptors(); + closeTempFiles(); + released_ = true; + return captured; + } + + private: + static int createTempFile(char* templ) + { + const int fd = mkstemp(templ); + if (fd >= 0) + { + unlink(templ); + } + return fd; + } + + static void flushStreams() + { + std::fflush(stdout); + std::fflush(stderr); + std::cout.flush(); + std::cerr.flush(); + llvm::outs().flush(); + llvm::errs().flush(); + } + + bool initialize() + { + flushStreams(); + + originalStdoutFd_ = dup(STDOUT_FILENO); + originalStderrFd_ = dup(STDERR_FILENO); + if (originalStdoutFd_ < 0 || originalStderrFd_ < 0) + { + return false; + } + + char stdoutTemplate[] = "/tmp/ctrace-stack-stdout-XXXXXX"; + char stderrTemplate[] = "/tmp/ctrace-stack-stderr-XXXXXX"; + + capturedStdoutFd_ = createTempFile(stdoutTemplate); + capturedStderrFd_ = createTempFile(stderrTemplate); + if (capturedStdoutFd_ < 0 || capturedStderrFd_ < 0) + { + return false; + } + + if (dup2(capturedStdoutFd_, STDOUT_FILENO) == -1) + { + return false; + } + if (dup2(capturedStderrFd_, STDERR_FILENO) == -1) + { + return false; + } + + return true; + } + + void restoreDescriptors() + { + if (!enabled_) + { + return; + } + + if (originalStdoutFd_ >= 0) + { + (void)dup2(originalStdoutFd_, STDOUT_FILENO); + } + if (originalStderrFd_ >= 0) + { + (void)dup2(originalStderrFd_, STDERR_FILENO); + } + + if (originalStdoutFd_ >= 0) + { + close(originalStdoutFd_); + originalStdoutFd_ = -1; + } + if (originalStderrFd_ >= 0) + { + close(originalStderrFd_); + originalStderrFd_ = -1; + } + } + + void closeTempFiles() + { + if (capturedStdoutFd_ >= 0) + { + close(capturedStdoutFd_); + capturedStdoutFd_ = -1; + } + if (capturedStderrFd_ >= 0) + { + close(capturedStderrFd_); + capturedStderrFd_ = -1; + } + } + + bool enabled_ = false; + bool released_ = false; + int originalStdoutFd_ = -1; + int originalStderrFd_ = -1; + int capturedStdoutFd_ = -1; + int capturedStderrFd_ = -1; + }; +#endif +} // namespace + +namespace ctrace +{ + void StackAnalyzerToolImplementation::execute(const std::string& file, + ctrace::ProgramConfig config) const + { + executeBatch(std::vector{file}, std::move(config)); + } + + void StackAnalyzerToolImplementation::executeBatch(const std::vector& files, + ctrace::ProgramConfig config) const + { + m_lastDiagnosticsSummary = {}; + + std::vector inputFiles; + inputFiles.reserve(files.size()); + for (const auto& file : files) + { + if (!file.empty()) + { + inputFiles.push_back(file); + } + } + + if (inputFiles.empty()) + { + ctrace::Thread::Output::tool_err( + "Stack analyzer batch execution requested with no input files."); + return; + } + + if (inputFiles.size() == 1) + { + coretrace::log(coretrace::Level::Info, "Running CoreTrace Stack Analyzer on {}\n", + inputFiles.front()); + } + else + { + coretrace::log(coretrace::Level::Info, "Running CoreTrace Stack Analyzer on {} files\n", + inputFiles.size()); + } + + llvm::LLVMContext ctx; + const auto analyzerArgBuild = buildAnalyzerArgs(inputFiles, config); + const std::vector& analyzerArgs = analyzerArgBuild.args; + + if (config.global.verbose) + { + coretrace::log(coretrace::Level::Debug, coretrace::Module(kStackAnalyzerModule), + "CoreTrace -> stack_analyzer bridge report ({} entries)\n", + analyzerArgBuild.bridgeReport.size()); + for (const auto& line : analyzerArgBuild.bridgeReport) + { + coretrace::log(coretrace::Level::Debug, coretrace::Module(kStackAnalyzerModule), + " {}\n", line); + } + } + + for (const auto& arg : analyzerArgs) + { + coretrace::log(coretrace::Level::Debug, "Analyzer argument: {}\n", arg); + } + + auto parseResult = ctrace::stack::cli::parseArguments(analyzerArgs); + if (parseResult.status == ctrace::stack::cli::ParseStatus::Error) + { + if (parseResult.error.empty()) + { + ctrace::Thread::Output::tool_err("Failed to parse stack analyzer arguments."); + } + else + { + ctrace::Thread::Output::tool_err(parseResult.error); + } + return; + } + if (parseResult.status == ctrace::stack::cli::ParseStatus::Help) + { + ctrace::Thread::Output::tool_out( + "Stack analyzer help requested; analysis was not executed."); + return; + } + +#if !defined(_WIN32) + ScopedFdCapture processOutputCapture; +#endif + + const ctrace::stack::app::RunResult runResult = + ctrace::stack::app::runAnalyzerApp(std::move(parseResult.parsed), ctx); + + std::string capturedStdout; + std::string capturedStderr; +#if !defined(_WIN32) + if (processOutputCapture.enabled()) + { + const auto captured = processOutputCapture.release(); + if (!captured.stdoutText.empty()) + { + capturedStdout = captured.stdoutText; + captureToolOutputOnly("stdout", captured.stdoutText); + if (config.global.ipc == "standardIO") { - for (const auto& d : res.diagnostics) - { - if (d.funcName != f.name) - continue; - - if (res.config.warningsOnly && - d.severity == ctrace::stack::DiagnosticSeverity::Info) - continue; - - if (d.line != 0) - { - llvm::outs() - << " at line " << d.line << ", column " << d.column << "\n"; - } - llvm::outs() << d.message << "\n"; - } + ctrace::Thread::Output::tool_out(captured.stdoutText); } - - llvm::outs() << "\n"; + else if (config.global.ipc == "socket" && ipc) + { + ipc->write(captured.stdoutText); + } + } + if (!captured.stderrText.empty()) + { + capturedStderr = captured.stderrText; + captureToolOutputOnly("stderr", captured.stderrText); + logMultiline(coretrace::Level::Warn, kStackAnalyzerModule, captured.stderrText); } } - if (config.global.ipc == "socket") +#endif + + if (const auto parsedSummary = parseDiagnosticsSummaryFromText(capturedStdout); + parsedSummary.has_value()) + { + m_lastDiagnosticsSummary = *parsedSummary; + } + else if (const auto parsedSummary = parseDiagnosticsSummaryFromText(capturedStderr); + parsedSummary.has_value()) { - ipc->write(ctrace::stack::toJson(res, filename)); + m_lastDiagnosticsSummary = *parsedSummary; + } + + if (!runResult.isOk()) + { + ctrace::Thread::Output::tool_err(runResult.error); + return; + } + + if (runResult.exitCode != 0) + { + ctrace::Thread::Output::tool_err("Stack analyzer exited with code " + + std::to_string(runResult.exitCode)); + return; } } @@ -95,4 +653,9 @@ namespace ctrace return "ctrace_stack_analyzer"; } + DiagnosticSummary StackAnalyzerToolImplementation::lastDiagnosticsSummary() const + { + return m_lastDiagnosticsSummary; + } + } // namespace ctrace diff --git a/tests/EmptyForStatement.cc b/tests/EmptyForStatement.cc index 77a6c0c..99b5d1a 100644 --- a/tests/EmptyForStatement.cc +++ b/tests/EmptyForStatement.cc @@ -1,3 +1,5 @@ +#include + int main() { for (int i = 0; i < 10; i++) diff --git a/tests/bound_index.cc b/tests/bound_index.cc index c1862dd..5de299e 100644 --- a/tests/bound_index.cc +++ b/tests/bound_index.cc @@ -5,6 +5,7 @@ int a[10]; int main(int argc, char* argv[]) { size_t i = 0; + printf("%c\n", a[0]); for (; i < 10; i++) { a[i] = i;