diff --git a/CMakeLists.txt b/CMakeLists.txt index f04dd1c..5fc4a95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,3 +107,24 @@ add_custom_target(format-check COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/scripts/format-check.sh" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMMENT "Verify clang-format compliance") + +# ============ +# TESTS +# ============ +enable_testing() + +add_executable(ctrace_config_tests + tests/config_parser_tests.cpp + src/App/ToolConfig.cpp + src/App/Config.cpp + src/ArgumentParser/BaseArgumentParser.cpp + src/ArgumentParser/ArgumentManager.cpp + src/ArgumentParser/ArgumentParserFactory.cpp + src/ArgumentParser/CLI11/CLI11ArgumentParser.cpp + src/ArgumentParser/GetOpt/GetoptArgumentParser.cpp + src/ctrace_tools/strings.cpp +) + +target_link_libraries(ctrace_config_tests PRIVATE nlohmann_json::nlohmann_json coretrace::logger) + +add_test(NAME ctrace_config_tests COMMAND ctrace_config_tests) diff --git a/README.md b/README.md index 40ee9f8..9bdaab9 100644 --- a/README.md +++ b/README.md @@ -53,17 +53,34 @@ Usage: Options: --help Displays this help message. --verbose Enables detailed (verbose) output. + --quiet Suppresses non-essential output. --sarif-format Generates a report in SARIF format. --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. + --timing Enables stack analyzer timing output. + --demangle Displays demangled function names in supported tools. --static Enables static analysis. --dyn Enables dynamic analysis. --invoke Invokes specific tools (comma-separated). - Available tools: flawfinder, ikos, cppcheck, tscancode. + Available tools: flawfinder, ikos, cppcheck, tscancode, ctrace_stack_analyzer. --input Specifies the source files to analyse (comma-separated). - --ipc IPC method: standardIO, socket, or serve. - --ipc-path IPC path (default: /tmp/coretrace_ipc). + --ipc Specifies the IPC method to use (e.g., fifo, socket). + --ipc-path Specifies the IPC path (default: /tmp/coretrace_ipc). --serve-host HTTP server host when --ipc=serve. --serve-port HTTP server port when --ipc=serve. --shutdown-token Token required for POST /shutdown (server mode). @@ -80,6 +97,12 @@ Description: and memory misuse. ``` +### CONFIGURATION + +- Canonical default config: `config/tool-config.json` +- Full schema and semantics: `docs/configuration.md` +- Precedence: built-in defaults < config file < CLI + ```bash ./ctrace --input ../tests/EmptyForStatement.cc --entry-points=main --verbose --static --dyn ``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..764a7b9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,21 @@ +# TODO + +## Stack Analyzer Summary Debt + +- [ ] Expose a structured diagnostics summary in `ctrace::stack::app::RunResult` from `coretrace-stack-analyzer`. + - Add `info`, `warning`, and `error` counters in the tool result contract. + - Compute the summary once from final filtered diagnostics inside the analyzer core. + - Keep output strategies (`human`, `json`, `sarif`) focused on serialization only. + - Make `coretrace` consume `RunResult.summary` as the primary source. + - Remove text/JSON/SARIF parsing fallback in `StackAnalyzerToolImplementation.cpp` after migration. + - Add compatibility notes and tests for mixed versions during transition. + +## Interprocedural Ownership Path Analysis Debt + +- [ ] Implement interprocedural ownership tracking that follows each abstract object/pointer through control-flow paths up to release/destructor points. + - Build an interprocedural CFG (call/return edges) and run a forward dataflow typestate analysis. + - Track per-object states (`Owned`, `Transferred`, `Released`, `Escaped`, `Unknown`) keyed by allocation/wrapper origins. + - Introduce transfer semantics (`transfer_arg` / adopt ownership) to model delayed-release patterns (GC/wrapper handoff). + - Use function summaries to scale cross-TU propagation while preserving precision on ownership effects. + - Report `MissingRelease`, `DoubleRelease`, `UseAfterRelease`, and `ReleasedHandleEscapes` from path-feasible states. + - Add regression tests for wrapper allocators, GC registration APIs, destructor-backed cleanup, and mixed modeled/unmodeled calls. diff --git a/cmake/stackUsageAnalyzer.cmake b/cmake/stackUsageAnalyzer.cmake index cbe22ee..72ec0cd 100644 --- a/cmake/stackUsageAnalyzer.cmake +++ b/cmake/stackUsageAnalyzer.cmake @@ -3,7 +3,15 @@ include(FetchContent) FetchContent_Declare( stack_analyzer GIT_REPOSITORY https://github.com/CoreTrace/coretrace-stack-analyzer.git - GIT_TAG main + GIT_TAG v0.17.0 ) FetchContent_MakeAvailable(stack_analyzer) + +# Copy upstream default models into config/models/ so that tool-config.json +# can reference them with paths relative to the config directory, without +# ever pointing into _deps/. +# Custom model files already present in config/models/ are not affected +# (they live at the root of the directory, upstream models are in subdirs). +file(COPY "${stack_analyzer_SOURCE_DIR}/models/" + DESTINATION "${CMAKE_SOURCE_DIR}/config/models") diff --git a/config/models/.gitignore b/config/models/.gitignore new file mode 100644 index 0000000..3c015b5 --- /dev/null +++ b/config/models/.gitignore @@ -0,0 +1,6 @@ +# Upstream default models — generated at CMake configure time from _deps/. +# Do not commit: they are copied from coretrace-stack-analyzer via FetchContent. +# Custom model files at this level (e.g. resource-lifetime-gc-temp.txt) are NOT ignored. +resource-lifetime/ +buffer-overflow/ +stack-escape/ diff --git a/config/tool-config.json b/config/tool-config.json index d9df6ed..2a29a36 100644 --- a/config/tool-config.json +++ b/config/tool-config.json @@ -1,24 +1,90 @@ { - "invoke": [ - "ctrace_stack_analyzer" - ], + "schema_version": 1, + "analysis": { + "static": false, + "dynamic": false, + "invoke": [ + "ctrace_stack_analyzer" + ] + }, + "files": { + "input": [], + "entry_points": [ + ], + "compile_commands": "", + "include_compdb_deps": false + }, + "output": { + "sarif_format": false, + "report_file": "coretrace-results.json", + "output_file": "ctrace.out", + "verbose": false, + "quiet": false, + "demangle": true + }, + "runtime": { + "async": false, + "ipc": "standardIO", + "ipc_path": "/tmp/coretrace_ipc" + }, + "server": { + "host": "127.0.0.1", + "port": 8080, + "shutdown_token": "", + "shutdown_timeout_ms": 0 + }, "stack_analyzer": { + "mode": "ir", + "output_format": "json", + "config": "", + "print_effective_config": false, "compile_commands": "", "include_compdb_deps": false, - "analysis-profile": "full", - "timing": true, + "compdb_fast": false, + "jobs": "", + "include_dirs": [], + "defines": [], + "compile_args": [], + "entry_points": [ + ], + "only_functions": [], + "only_files": [], + "only_dirs": [], + "exclude_dirs": [], + "analysis_profile": "full", "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, + "smt_backend": "z3", + "smt_secondary_backend": "", + "smt_mode": "single", + "smt_timeout_ms": 80, + "smt_budget_nodes": 20000, + "smt_rules": [ + "recursion", + "integer-overflow", + "size-minus-k", + "stack-buffer", + "oob-read", + "type-confusion" + ], + "resource_model": "models/resource-lifetime/generic.txt", + "escape_model": "models/stack-escape/generic.txt", + "buffer_model": "models/buffer-overflow/generic.txt", + "resource_cross_tu": true, + "uninitialized_cross_tu": true, + "resource_summary_cache_dir": ".cache/resource-lifetime", + "resource_summary_cache_memory_only": false, + "compile_ir_cache_dir": "", + "compile_ir_format": "bc", + "include_stl": 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" + "base_dir": "", + "dump_filter": false, + "dump_ir": "", + "verbose": false, + "demangle": true, + "quiet": true, + "timing": false, + "warnings_only": false, + "extra_args": [] } } diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..7ef9c6f --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,338 @@ +# Configuration Reference + +`config/tool-config.json` is the canonical configuration file. + +## Precedence Rules + +1. Built-in defaults (`GlobalConfig`) +2. Configuration file (`--config`) +3. CLI options (last override) + +CLI values always override config file values. + +## Schema Version + +- `schema_version` +Type: `uint` +Default: `1` +Allowed: `1` +Description: schema compatibility gate. +Impact: rejects unsupported schemas with explicit diagnostics. + +## analysis + +- `analysis.static` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: enable static analysis pipeline. +Impact: runs static tool set when true. +CLI: `--static` + +- `analysis.dynamic` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: enable dynamic analysis pipeline. +Impact: runs dynamic tool set when true. +CLI: `--dyn` + +- `analysis.invoke` +Type: `string|string[]` +Default: `[]` +Allowed: `flawfinder|ikos|cppcheck|tscancode|ctrace_stack_analyzer` +Description: explicit tool selection. +Impact: runs only selected tools through specific-tool path. +CLI: `--invoke` + +## files + +- `files.input` +Type: `string|string[]` +Default: `[]` +Allowed: source files and/or JSON manifests. +Description: input source set. +Impact: resolved and analyzed; relative paths are resolved from config file directory. +CLI: `--input` + +- `files.entry_points` +Type: `string|string[]` +Default: `["main"]` +Allowed: function names. +Description: entry-point filter list. +Impact: forwarded to tools supporting entry-point filtering. +CLI: `--entry-points` + +- `files.compile_commands` +Type: `string` +Default: `""` +Allowed: path to `compile_commands.json` or directory containing it. +Description: compilation database for analyzer context. +Impact: enables compile database driven file resolution and analyzer context. +CLI: `--compile-commands` + +- `files.include_compdb_deps` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: include dependency entries (`_deps`) from auto-discovered compdb inputs. +Impact: broadens or narrows analyzed source set. +CLI: `--include-compdb-deps` + +## output + +- `output.sarif_format` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: enable SARIF-oriented output behavior. +Impact: toggles SARIF-related output mapping/forwarding. +CLI: `--sarif-format` + +- `output.report_file` +Type: `string` +Default: `"ctrace-report.txt"` +Allowed: writable path. +Description: report output path. +Impact: IKOS receives this path via `--report-file`; stack analyzer stdout report is persisted to this file by coretrace after a successful run. +CLI: `--report-file` + +- `output.output_file` +Type: `string` +Default: `"ctrace.out"` +Allowed: writable path. +Description: generic output artifact path. +Impact: unified output target for integrations that use this field. +CLI: `--output-file` + +- `output.verbose` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: verbose logs. +Impact: enables debug-level logs and bridge decision traces. +CLI: `--verbose` + +- `output.quiet` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: reduced output mode. +Impact: forwarded to stack analyzer and used by core logging behavior. +CLI: `--quiet` + +- `output.demangle` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: demangle symbol names when supported. +Impact: forwarded to stack analyzer and other supported tools. +CLI: `--demangle` + +## runtime + +- `runtime.async` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: async execution policy. +Impact: enables thread-pool based tool scheduling. +CLI: `--async` + +- `runtime.ipc` +Type: `string` +Default: `"standardIO"` +Allowed: `standardIO|socket|serve` +Description: IPC mode. +Impact: selects standard output, socket transport, or HTTP server mode. +CLI: `--ipc` + +- `runtime.ipc_path` +Type: `string` +Default: `"/tmp/coretrace_ipc"` +Allowed: socket path. +Description: IPC socket path. +Impact: used when IPC mode is socket. +CLI: `--ipc-path` + +## server + +- `server.host` +Type: `string` +Default: `"127.0.0.1"` +Allowed: valid host/ip. +Description: HTTP server bind host. +Impact: controls server listen interface in `serve` mode. +CLI: `--serve-host` + +- `server.port` +Type: `uint` +Default: `8080` +Allowed: `0..65535` +Description: HTTP server bind port. +Impact: controls server listen port in `serve` mode. +CLI: `--serve-port` + +- `server.shutdown_token` +Type: `string` +Default: `""` +Allowed: any non-empty token for shutdown auth. +Description: shutdown endpoint auth token. +Impact: required for authenticated shutdown requests. +CLI: `--shutdown-token` + +- `server.shutdown_timeout_ms` +Type: `uint` +Default: `0` +Allowed: `0..INT_MAX` +Description: graceful shutdown timeout. +Impact: waits for in-flight requests up to timeout (`0` = wait indefinitely). +CLI: `--shutdown-timeout-ms` + +## stack_analyzer + +- `stack_analyzer.mode` +Type: `string` +Default: `"ir"` +Allowed: analyzer-supported modes. +Description: stack analyzer execution mode. +Impact: forwarded as `--mode=`. +CLI: not exposed (`config/tool-config.json` only) + +- `stack_analyzer.output_format` +Type: `string` +Default: `""` +Allowed: analyzer-supported formats (`json`, `sarif`, `text`, ...). +Description: explicit stack analyzer output format. +Impact: forwarded as `--format=`; if empty and SARIF is enabled, bridge uses `--format=json`. +CLI: not exposed (`config/tool-config.json` only) + +- `stack_analyzer.timing` +Type: `bool` +Default: `false` +Allowed: `true|false` +Description: enable timing stats from stack analyzer. +Impact: forwarded as `--timing`. +CLI: `--timing` + +- `stack_analyzer.analysis_profile` +Type: `string` +Default: `""` +Allowed: `fast|full|""` +Description: profile selection. +Impact: forwarded as `--analysis-profile` when non-empty. +CLI: `--analysis-profile` + +- `stack_analyzer.smt` +Type: `bool|string` +Default: `""` +Allowed: bool-like values (`true/false`, `on/off`, `1/0`, `yes/no`). +Description: SMT enable switch. +Impact: normalized and forwarded as `--smt on|off`. +CLI: `--smt` + +- `stack_analyzer.smt_backend` +Type: `string` +Default: `""` +Allowed: backend names supported by analyzer. +Description: primary SMT backend. +Impact: forwarded as `--smt-backend` when non-empty. +CLI: `--smt-backend` + +- `stack_analyzer.smt_secondary_backend` +Type: `string` +Default: `""` +Allowed: backend names supported by analyzer. +Description: secondary SMT backend. +Impact: forwarded as `--smt-secondary-backend` when non-empty. +CLI: `--smt-secondary-backend` + +- `stack_analyzer.smt_mode` +Type: `string` +Default: `""` +Allowed: `single|portfolio|cross-check|dual-consensus|""` +Description: SMT orchestration mode. +Impact: forwarded as `--smt-mode` when non-empty. +CLI: `--smt-mode` + +- `stack_analyzer.smt_timeout_ms` +Type: `uint` +Default: `0` +Allowed: `0..UINT32_MAX` +Description: SMT timeout per query. +Impact: forwarded as `--smt-timeout-ms` when > 0. +CLI: `--smt-timeout-ms` + +- `stack_analyzer.smt_budget_nodes` +Type: `uint` +Default: `0` +Allowed: `0..UINT64_MAX` +Description: SMT budget cap. +Impact: forwarded as `--smt-budget-nodes` when > 0. +CLI: `--smt-budget-nodes` + +- `stack_analyzer.smt_rules` +Type: `string|string[]` +Default: `[]` +Allowed: analyzer rule ids. +Description: SMT-enabled rule subset. +Impact: forwarded as `--smt-rules` CSV when non-empty. +CLI: `--smt-rules` + +- `stack_analyzer.stack_limit` +Type: `uint` +Default: `8388608` +Allowed: `0..UINT64_MAX` +Description: stack bound limit in bytes. +Impact: forwarded as `--stack-limit` when > 0. +CLI: `--stack-limit` + +- `stack_analyzer.resource_model` +Type: `string` +Default: `""` +Allowed: file path. +Description: resource lifetime model path. +Impact: forwarded as `--resource-model` when non-empty; relative paths resolved from config dir. +CLI: `--resource-model` + +- `stack_analyzer.escape_model` +Type: `string` +Default: `""` +Allowed: file path. +Description: escape model path. +Impact: forwarded as `--escape-model` when non-empty; relative paths resolved from config dir. +CLI: `--escape-model` + +- `stack_analyzer.buffer_model` +Type: `string` +Default: `""` +Allowed: file path. +Description: buffer model path. +Impact: forwarded as `--buffer-model` when non-empty; relative paths resolved from config dir. +CLI: `--buffer-model` + +- `stack_analyzer.extra_args` +Type: `string|string[]` +Default: `[]` +Allowed: analyzer CLI tokens. +Description: generic extension point for analyzer runtime flags not explicitly modeled. +Impact: forwarded verbatim after mapped options. +CLI: not exposed (`config/tool-config.json` only) + +## Validation Behavior + +- Unknown root keys are rejected with allowed-key diagnostics. +- Unknown section keys are rejected with allowed-key diagnostics. +- Type errors are rejected with path-qualified diagnostics. +- Enum-like values (`ipc`, `analysis_profile`, `smt_mode`) are validated explicitly. + +## Legacy Compatibility + +The loader still accepts legacy shapes: + +- root `invoke` +- root `input` as string or array +- `stack_analyzer` legacy keys (`analysis-profile`, `smt-*`, `entry_points`, etc.) +- `tools.ctrace_stack_analyzer` / `tools.stack_analyzer` + +When both legacy and canonical sections are present, canonical sections (`analysis`, `files`, `output`, `runtime`, `server`) are applied last and therefore take precedence inside the config file. diff --git a/include/App/SupportedTools.hpp b/include/App/SupportedTools.hpp new file mode 100644 index 0000000..4a5fff1 --- /dev/null +++ b/include/App/SupportedTools.hpp @@ -0,0 +1,75 @@ +#ifndef APP_SUPPORTED_TOOLS_HPP +#define APP_SUPPORTED_TOOLS_HPP + +#include +#include +#include +#include + +namespace ctrace +{ + inline constexpr std::array SUPPORTED_TOOLS = { + "flawfinder", "ikos", "cppcheck", "tscancode", "ctrace_stack_analyzer", + }; + + inline bool isSupportedToolName(std::string_view name) + { + for (const auto tool : SUPPORTED_TOOLS) + { + if (tool == name) + { + return true; + } + } + return false; + } + + inline std::string supportedToolsCsv() + { + std::string joined; + for (std::size_t i = 0; i < SUPPORTED_TOOLS.size(); ++i) + { + if (i > 0) + { + joined.push_back(','); + } + joined.append(SUPPORTED_TOOLS[i]); + } + return joined; + } + + inline std::vector + normalizeAndValidateToolList(const std::vector& tools, std::string& error) + { + std::vector normalized; + normalized.reserve(tools.size()); + + for (const auto& tool : tools) + { + if (!isSupportedToolName(tool)) + { + error = "Unknown tool '" + tool + "'. Allowed tools: [" + supportedToolsCsv() + "]"; + return {}; + } + + bool alreadyAdded = false; + for (const auto& current : normalized) + { + if (current == tool) + { + alreadyAdded = true; + break; + } + } + if (!alreadyAdded) + { + normalized.push_back(tool); + } + } + + error.clear(); + return normalized; + } +} // namespace ctrace + +#endif // APP_SUPPORTED_TOOLS_HPP diff --git a/include/Config/config.hpp b/include/Config/config.hpp index 347e909..05b2f55 100644 --- a/include/Config/config.hpp +++ b/include/Config/config.hpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include "ArgumentParser/ArgumentManager.hpp" #include "ArgumentParser/ArgumentParserFactory.hpp" +#include "App/SupportedTools.hpp" #include "ctrace_tools/strings.hpp" #include "ctrace_defs/types.hpp" @@ -48,10 +50,13 @@ static void printHelp(void) --static Enables static analysis. --dyn Enables dynamic analysis. --invoke Invokes specific tools (comma-separated). - Available tools: flawfinder, ikos, cppcheck, tscancode. + Available tools: flawfinder, ikos, cppcheck, tscancode, ctrace_stack_analyzer. --input Specifies the source files to analyse (comma-separated). + --timing Enables stack analyzer timing output. --ipc Specifies the IPC method to use (e.g., fifo, socket). --ipc-path Specifies the IPC path (default: /tmp/coretrace_ipc). + --serve-host HTTP server host when --ipc=serve. + --serve-port HTTP server port when --ipc=serve. --async Enables asynchronous execution. --shutdown-token Token required for POST /shutdown (server mode). --shutdown-timeout-ms Graceful shutdown timeout in ms (0 = wait indefinitely). @@ -116,7 +121,7 @@ namespace ctrace std::vector specificTools; ///< List of specific tools to invoke. - std::string entry_points = ""; ///< Entry points for analysis. + std::string entry_points = "main"; ///< 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. @@ -134,7 +139,36 @@ namespace ctrace 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. + std::string stack_analyzer_mode = "ir"; ///< Stack analyzer execution mode. + std::string stack_analyzer_output_format; ///< Stack analyzer output format. + std::string stack_analyzer_config; ///< Optional analyzer-native key=value config path. + bool stack_analyzer_print_effective_config = false; ///< Print analyzer effective config. + bool stack_analyzer_compdb_fast = false; ///< Enables fast compile DB mode in analyzer. + bool stack_analyzer_include_stl = false; ///< Include STL/system functions in analyzer. + bool stack_analyzer_dump_filter = false; ///< Enables analyzer filter tracing. + bool stack_analyzer_warnings_only = false; ///< Emit warning/error diagnostics only. + bool stack_analyzer_resource_summary_cache_memory_only = + false; ///< Keep resource summary cache in memory only. + std::optional + stack_analyzer_resource_cross_tu; ///< Override analyzer resource cross-TU toggle. + std::optional + stack_analyzer_uninitialized_cross_tu; ///< Override uninitialized cross-TU toggle. + std::string stack_analyzer_jobs; ///< Analyzer jobs value ("auto" or positive integer). + std::string stack_analyzer_base_dir; ///< Base directory for SARIF URI normalization. + std::string stack_analyzer_dump_ir; ///< Dump LLVM IR path (file/dir). + std::string + stack_analyzer_resource_summary_cache_dir; ///< Resource summary cache directory. + std::string stack_analyzer_compile_ir_cache_dir; ///< Compile IR cache directory. + std::string stack_analyzer_compile_ir_format; ///< Compile IR format (bc|ll). + std::vector stack_analyzer_only_files; ///< --only-file filters. + std::vector stack_analyzer_only_dirs; ///< --only-dir filters. + std::vector stack_analyzer_exclude_dirs; ///< --exclude-dir filters. + std::vector stack_analyzer_only_functions; ///< --only-func filters. + std::vector stack_analyzer_include_dirs; ///< -I include directories. + std::vector stack_analyzer_defines; ///< -D preprocessor defines. + std::vector stack_analyzer_compile_args; ///< --compile-arg values. + std::vector stack_analyzer_extra_args; ///< Extra stack analyzer args. + uint64_t stack_limit = 8 * 1024 * 1024; ///< Stack limit in bytes. }; /** @@ -196,6 +230,8 @@ namespace ctrace { config.global.hasSarifFormat = true; }; commands["--report-file"] = [this](const std::string& value) { config.global.report_file = value; }; + commands["--output-file"] = [this](const std::string& value) + { config.global.output_file = value; }; commands["--async"] = [this](const std::string&) { config.global.hasAsync = std::launch::async; @@ -203,38 +239,20 @@ namespace ctrace }; commands["--invoke"] = [this](const std::string& value) { - config.global.hasInvokedSpecificTools = true; - auto parts = ctrace_tools::strings::splitByComma(value); - - for (const auto& part : parts) + std::vector parts; + for (const auto part : ctrace_tools::strings::splitByComma(value)) { - // TODO: Refactor this block for better readability. - if (part == "flawfinder") - { - // config.global.hasStaticAnalysis = true; - config.global.specificTools.emplace_back("flawfinder"); - } - if (part == "ikos") - { - // config.global.hasStaticAnalysis = true; - config.global.specificTools.emplace_back("ikos"); - } - if (part == "cppcheck") - { - // config.global.hasStaticAnalysis = true; - config.global.specificTools.emplace_back("cppcheck"); - } - if (part == "tscancode") - { - // config.global.hasStaticAnalysis = true; - config.global.specificTools.emplace_back("tscancode"); - } - if (part == "ctrace_stack_analyzer") - { - // config.global.hasStaticAnalysis = true; - config.global.specificTools.emplace_back("ctrace_stack_analyzer"); - } + parts.emplace_back(part); + } + std::string normalizeError; + const auto normalized = normalizeAndValidateToolList(parts, normalizeError); + if (!normalizeError.empty()) + { + std::cerr << normalizeError << std::endl; + std::exit(EXIT_FAILURE); } + config.global.specificTools = normalized; + config.global.hasInvokedSpecificTools = !normalized.empty(); }; commands["--input"] = [this](const std::string& value) { config.addFile(value); }; commands["--static"] = [this](const std::string&) @@ -298,6 +316,7 @@ namespace ctrace { config.global.escape_model = value; }; commands["--buffer-model"] = [this](const std::string& value) { config.global.buffer_model = value; }; + commands["--timing"] = [this](const std::string&) { config.global.timing = true; }; commands["--stack-limit"] = [this](const std::string& value) { try diff --git a/include/Process/Ipc/HttpServer.hpp b/include/Process/Ipc/HttpServer.hpp index 7b8eda6..bada6da 100644 --- a/include/Process/Ipc/HttpServer.hpp +++ b/include/Process/Ipc/HttpServer.hpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -110,6 +112,12 @@ class ApiHandler std::string* target; }; + struct Uint64Field + { + const char* key; + uint64_t* target; + }; + ILogger& logger_; static void log_request(ILogger& logger, const json& request) @@ -149,6 +157,22 @@ class ApiHandler return true; } + static bool read_uint64(const json& params, const char* key, uint64_t& out, ParseError& err) + { + const auto it = params.find(key); + if (it == params.end() || it->is_null()) + { + return true; + } + if (!it->is_number_unsigned()) + { + err = {"InvalidParams", std::string("Expected unsigned integer for '") + key + "'."}; + return false; + } + out = it->get(); + return true; + } + static bool read_string_list(const json& params, const char* key, std::vector& out, ParseError& err) { @@ -221,6 +245,19 @@ class ApiHandler return true; } + static bool apply_uint64_fields(const json& params, ParseError& err, + std::initializer_list fields) + { + for (const auto& field : fields) + { + if (!read_uint64(params, field.key, *field.target, err)) + { + return false; + } + } + return true; + } + template static bool apply_list_param(const json& params, const char* key, ParseError& err, ApplyFn&& apply) @@ -310,10 +347,13 @@ class ApiHandler if (!apply_bool_fields(params, err, { {"verbose", &config.global.verbose}, + {"quiet", &config.global.quiet}, + {"demangle", &config.global.demangle}, {"sarif_format", &config.global.hasSarifFormat}, {"static_analysis", &config.global.hasStaticAnalysis}, {"dynamic_analysis", &config.global.hasDynamicAnalysis}, {"include_compdb_deps", &config.global.include_compdb_deps}, + {"timing", &config.global.timing}, })) { return false; @@ -352,11 +392,33 @@ class ApiHandler {"resource_model", &config.global.resource_model}, {"escape_model", &config.global.escape_model}, {"buffer_model", &config.global.buffer_model}, + {"stack_analyzer_mode", &config.global.stack_analyzer_mode}, + {"stack_analyzer_output_format", &config.global.stack_analyzer_output_format}, {"ipc_path", &config.global.ipcPath}, })) { return false; } + uint64_t smt_timeout = config.global.smt_timeout_ms; + uint64_t smt_budget = config.global.smt_budget_nodes; + uint64_t stack_limit = config.global.stack_limit; + if (!apply_uint64_fields(params, err, + { + {"smt_timeout_ms", &smt_timeout}, + {"smt_budget_nodes", &smt_budget}, + {"stack_limit", &stack_limit}, + })) + { + return false; + } + if (smt_timeout > std::numeric_limits::max()) + { + err = {"InvalidParams", "smt_timeout_ms is too large."}; + return false; + } + config.global.smt_timeout_ms = static_cast(smt_timeout); + config.global.smt_budget_nodes = smt_budget; + config.global.stack_limit = stack_limit; if (!apply_list_param(params, "entry_points", err, [&](const std::vector& values) { config.global.entry_points = join_with_comma(values); })) @@ -377,6 +439,12 @@ class ApiHandler { return false; } + if (!apply_list_param(params, "stack_analyzer_extra_args", err, + [&](const std::vector& values) + { config.global.stack_analyzer_extra_args = values; })) + { + return false; + } if (!apply_list_param(params, "input", err, [&](const std::vector& values) { @@ -488,7 +556,14 @@ class ApiHandler 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_timeout_ms"] = config.global.smt_timeout_ms; + result["smt_budget_nodes"] = config.global.smt_budget_nodes; result["smt_rules"] = config.global.smt_rules; + result["timing"] = config.global.timing; + result["stack_limit"] = config.global.stack_limit; + result["stack_analyzer_mode"] = config.global.stack_analyzer_mode; + result["stack_analyzer_output_format"] = config.global.stack_analyzer_output_format; + result["stack_analyzer_extra_args"] = config.global.stack_analyzer_extra_args; if (output_capture) { json outputs = json::object(); diff --git a/src/App/Config.cpp b/src/App/Config.cpp index a835c0d..c763817 100644 --- a/src/App/Config.cpp +++ b/src/App/Config.cpp @@ -19,7 +19,7 @@ namespace ctrace argManager.addOption("--verbose", false, 'v'); argManager.addFlag("--help", 'h'); argManager.addFlag("--quiet", 'q'); - argManager.addOption("--output", true, 'o'); + argManager.addOption("--output-file", true, 'o'); argManager.addOption("--invoke", true, 'i'); argManager.addOption("--sarif-format", false, 'f'); argManager.addOption("--input", true, 's'); @@ -40,6 +40,7 @@ namespace ctrace argManager.addOption("--resource-model", true, 'R'); argManager.addOption("--escape-model", true, 'E'); argManager.addOption("--buffer-model", true, 'B'); + argManager.addOption("--timing", false, 'H'); argManager.addOption("--demangle", false, 'g'); argManager.addOption("--stack-limit", true, 'l'); argManager.addOption("--report-file", true, 'r'); diff --git a/src/App/ToolConfig.cpp b/src/App/ToolConfig.cpp index 4966097..35e2bb8 100644 --- a/src/App/ToolConfig.cpp +++ b/src/App/ToolConfig.cpp @@ -1,5 +1,7 @@ #include "App/ToolConfig.hpp" +#include "App/SupportedTools.hpp" + #include #include #include @@ -10,6 +12,7 @@ #include #include #include +#include #include namespace ctrace @@ -18,6 +21,8 @@ namespace ctrace { using json = nlohmann::json; + constexpr uint64_t kToolConfigSchemaVersion = 1; + [[nodiscard]] std::filesystem::path resolvePathFromBase(const std::filesystem::path& baseDir, std::string_view rawPath) { @@ -29,80 +34,105 @@ namespace ctrace return path.lexically_normal(); } - [[nodiscard]] bool loadJsonFile(const std::filesystem::path& filePath, json& out, - std::string& errorMessage) + [[nodiscard]] std::string toLower(std::string value) { - std::ifstream input(filePath); - if (!input.is_open()) - { - errorMessage = "Unable to open file: " + filePath.string(); - return false; - } + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + return value; + } - std::ostringstream buffer; - buffer << input.rdbuf(); - out = json::parse(buffer.str(), nullptr, false); - if (out.is_discarded()) + [[nodiscard]] std::string trimCopy(std::string_view input) + { + std::size_t start = 0; + while (start < input.size() && std::isspace(static_cast(input[start]))) { - errorMessage = "Invalid JSON in file: " + filePath.string(); - return false; + ++start; } - if (!out.is_object()) + + std::size_t end = input.size(); + while (end > start && std::isspace(static_cast(input[end - 1]))) { - errorMessage = "Config root must be a JSON object."; - return false; + --end; } - return true; + + return std::string(input.substr(start, end - start)); } - [[nodiscard]] bool readStringValue(const json& object, const char* key, std::string& out, - std::string& errorMessage) + [[nodiscard]] std::string joinAllowed(std::initializer_list keys) { - const auto it = object.find(key); - if (it == object.end() || it->is_null()) + std::string joined; + bool first = true; + for (const auto* key : keys) { - return true; + if (key == nullptr) + { + continue; + } + if (!first) + { + joined += ", "; + } + first = false; + joined += key; } - if (!it->is_string()) + return joined; + } + + [[nodiscard]] bool validateKnownKeys(const json& object, + std::initializer_list allowedKeys, + std::string_view context, std::string& errorMessage) + { + if (!object.is_object()) { - errorMessage = std::string("Expected string for '") + key + "'."; + errorMessage = "Expected JSON object for '" + std::string(context) + "'."; 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()) + std::unordered_set allowed; + allowed.reserve(allowedKeys.size()); + for (const auto* key : allowedKeys) { - return true; + if (key != nullptr) + { + allowed.emplace(key); + } } - if (!it->is_boolean()) + + for (auto it = object.begin(); it != object.end(); ++it) { - errorMessage = std::string("Expected boolean for '") + key + "'."; - return false; + if (allowed.find(it.key()) == allowed.end()) + { + errorMessage = "Unknown key '" + it.key() + "' in '" + std::string(context) + + "'. Allowed keys: [" + joinAllowed(allowedKeys) + "]"; + return false; + } } - out = it->get(); return true; } - [[nodiscard]] bool readUint64Value(const json& object, const char* key, uint64_t& out, - std::string& errorMessage) + [[nodiscard]] bool loadJsonFile(const std::filesystem::path& filePath, json& out, + std::string& errorMessage) { - const auto it = object.find(key); - if (it == object.end() || it->is_null()) + std::ifstream input(filePath); + if (!input.is_open()) { - return true; + errorMessage = "Unable to open file: " + filePath.string(); + return false; } - if (!it->is_number_unsigned()) + + 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 = std::string("Expected unsigned integer for '") + key + "'."; + errorMessage = "Config root must be a JSON object."; return false; } - out = it->get(); return true; } @@ -124,411 +154,1623 @@ namespace ctrace return nullptr; } - [[nodiscard]] bool readStringValueAny(const json& object, - std::initializer_list keys, - std::string& out, std::string& errorMessage) + [[nodiscard]] bool parseStringList(const json& value, std::vector& out, + std::string& errorMessage, + const std::string& locationPath) { - const json* value = findFirstValueForKeys(object, keys); - if (value == nullptr || value->is_null()) + out.clear(); + if (value.is_string()) { + out.push_back(value.get()); return true; } - if (!value->is_string()) + if (!value.is_array()) { - errorMessage = "Expected string value in stack_analyzer config."; + errorMessage = "Expected string or array of strings for '" + locationPath + "'."; return false; } - out = value->get(); + out.reserve(value.size()); + for (const auto& item : value) + { + if (!item.is_string()) + { + errorMessage = "Expected only strings in '" + locationPath + "'."; + return false; + } + out.push_back(item.get()); + } return true; } - [[nodiscard]] bool parseBoolLikeString(const std::string& raw, bool& out) + [[nodiscard]] bool parseBoolLike(const json& value, bool& out, std::string& errorMessage, + const std::string& locationPath) + { + if (value.is_boolean()) + { + out = value.get(); + return true; + } + if (value.is_string()) + { + const auto lowered = toLower(value.get()); + 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; + } + } + errorMessage = "Expected boolean or bool-like string for '" + locationPath + "'."; + return false; + } + + [[nodiscard]] bool parseUint64(const json& value, uint64_t& out, std::string& errorMessage, + const std::string& locationPath) { - std::string lowered; - lowered.reserve(raw.size()); - for (const auto ch : raw) + if (!value.is_number_unsigned()) { - lowered.push_back(static_cast(std::tolower(static_cast(ch)))); + errorMessage = "Expected unsigned integer for '" + locationPath + "'."; + return false; } + out = value.get(); + return true; + } - if (lowered == "true" || lowered == "on" || lowered == "1" || lowered == "yes") + [[nodiscard]] bool readOptionalStringAny(const json& object, + std::initializer_list keys, + std::string& out, std::string& errorMessage, + const std::string& locationPath, bool& hasValue) + { + const auto* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) { - out = true; + hasValue = false; return true; } - if (lowered == "false" || lowered == "off" || lowered == "0" || lowered == "no") + if (!value->is_string()) { - out = false; - return true; + errorMessage = "Expected string for '" + locationPath + "'."; + return false; } - return false; + out = value->get(); + hasValue = true; + return true; } - [[nodiscard]] bool readBoolLikeValueAny(const json& object, - std::initializer_list keys, bool& out, - std::string& errorMessage) + [[nodiscard]] bool + readOptionalScalarAsStringAny(const json& object, std::initializer_list keys, + std::string& out, std::string& errorMessage, + const std::string& locationPath, bool& hasValue) { - const json* value = findFirstValueForKeys(object, keys); + const auto* value = findFirstValueForKeys(object, keys); if (value == nullptr || value->is_null()) { + hasValue = false; return true; } - if (value->is_boolean()) + + if (value->is_string()) { - out = value->get(); + out = value->get(); + hasValue = true; return true; } - if (value->is_string()) + if (value->is_number_unsigned()) { - const auto raw = value->get(); - if (parseBoolLikeString(raw, out)) - { - return true; - } + out = std::to_string(value->get()); + hasValue = true; + return true; + } + if (value->is_number_integer()) + { + out = std::to_string(value->get()); + hasValue = true; + return true; } - errorMessage = "Expected boolean or boolean-like string in stack_analyzer config."; + + errorMessage = "Expected string or integer for '" + locationPath + "'."; return false; } - [[nodiscard]] bool readUint64ValueAny(const json& object, - std::initializer_list keys, - uint64_t& out, std::string& errorMessage) + [[nodiscard]] bool readOptionalBoolAny(const json& object, + std::initializer_list keys, bool& out, + std::string& errorMessage, + const std::string& locationPath, bool& hasValue) { - const json* value = findFirstValueForKeys(object, keys); + const auto* value = findFirstValueForKeys(object, keys); if (value == nullptr || value->is_null()) { + hasValue = false; return true; } - if (!value->is_number_unsigned()) + if (!value->is_boolean()) { - errorMessage = "Expected unsigned integer value in stack_analyzer config."; + errorMessage = "Expected boolean for '" + locationPath + "'."; return false; } - out = value->get(); + out = value->get(); + hasValue = true; return true; } - [[nodiscard]] bool parseStringList(const json& value, std::vector& out, - std::string& errorMessage, const char* keyName) + [[nodiscard]] bool readOptionalBoolLikeAny(const json& object, + std::initializer_list keys, + bool& out, std::string& errorMessage, + const std::string& locationPath, bool& hasValue) { - out.clear(); - if (value.is_string()) + const auto* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) { - out.push_back(value.get()); + hasValue = false; 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; + hasValue = true; + return parseBoolLike(*value, out, errorMessage, locationPath); } - [[nodiscard]] bool readStringListValueAny(const json& object, - std::initializer_list keys, - std::vector& out, - std::string& errorMessage, const char* keyName) + [[nodiscard]] bool readOptionalUint64Any(const json& object, + std::initializer_list keys, + uint64_t& out, std::string& errorMessage, + const std::string& locationPath, bool& hasValue) { - const json* value = findFirstValueForKeys(object, keys); + const auto* value = findFirstValueForKeys(object, keys); if (value == nullptr || value->is_null()) { + hasValue = false; return true; } - return parseStringList(*value, out, errorMessage, keyName); + hasValue = true; + return parseUint64(*value, out, errorMessage, locationPath); } - void appendUnique(std::vector& target, const std::string& value) + [[nodiscard]] bool + readOptionalStringListAny(const json& object, std::initializer_list keys, + std::vector& out, std::string& errorMessage, + const std::string& locationPath, bool& hasValue) { - if (value.empty()) - { - return; - } - if (std::find(target.begin(), target.end(), value) == target.end()) + const auto* value = findFirstValueForKeys(object, keys); + if (value == nullptr || value->is_null()) { - target.push_back(value); + hasValue = false; + return true; } + hasValue = true; + return parseStringList(*value, out, errorMessage, locationPath); } - [[nodiscard]] std::string joinComma(const std::vector& values) + [[nodiscard]] bool parseEntryPoints(const std::vector& points, + ProgramConfig& config) { std::string joined; - for (std::size_t i = 0; i < values.size(); ++i) + for (std::size_t i = 0; i < points.size(); ++i) { - if (i != 0) + if (i > 0) { - joined += ","; + joined.push_back(','); } - joined += values[i]; + joined += points[i]; } - return joined; + config.global.entry_points = joined; + return true; } - [[nodiscard]] const json* findStackAnalyzerConfigSection(const json& root) + void assignInputFiles(const std::vector& entries, + const std::filesystem::path& configDir, ProgramConfig& config, + bool replace) { - if (const auto it = root.find("stack_analyzer"); it != root.end() && it->is_object()) + if (replace) { - return &(*it); + config.files.clear(); } - if (const auto it = root.find("tools"); it != root.end() && it->is_object()) + + for (const auto& entry : entries) { - 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()) + if (entry.empty()) { - return &(*itTool); + continue; } + const auto resolved = resolvePathFromBase(configDir, entry); + config.files.emplace_back(resolved.string()); } - return nullptr; } - [[nodiscard]] bool applyInvoke(const json& root, ProgramConfig& config, - std::string& errorMessage) + [[nodiscard]] bool validateIpcValue(const std::string& ipcValue, std::string& errorMessage, + const std::string& locationPath) { - const auto it = root.find("invoke"); - if (it == root.end() || it->is_null()) + if (ipcValue.empty()) { return true; } - - std::vector tools; - if (!parseStringList(*it, tools, errorMessage, "invoke")) + const auto& ipcList = ctrace_defs::IPC_TYPES; + if (std::find(ipcList.begin(), ipcList.end(), ipcValue) != ipcList.end()) { - return false; + return true; } - if (!tools.empty()) + std::string allowed; + for (std::size_t i = 0; i < ipcList.size(); ++i) { - config.global.hasInvokedSpecificTools = true; - for (const auto& tool : tools) + if (i > 0) { - appendUnique(config.global.specificTools, tool); + allowed += ", "; } + allowed += ipcList[i]; } - return true; + errorMessage = "Invalid value '" + ipcValue + "' for '" + locationPath + + "'. Allowed values: [" + allowed + "]"; + return false; } - [[nodiscard]] bool applyInputFiles(const json& root, const std::filesystem::path& configDir, - ProgramConfig& config, std::string& errorMessage) + [[nodiscard]] bool validateAnalysisProfile(const std::string& value, + const std::string& locationPath, + std::string& errorMessage) { - const auto it = root.find("input"); - if (it == root.end() || it->is_null()) + if (value.empty()) { return true; } - - std::vector entries; - if (!parseStringList(*it, entries, errorMessage, "input")) + if (value == "fast" || value == "full") { - return false; + return true; } + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [fast, full]"; + return false; + } - for (const auto& entry : entries) + [[nodiscard]] bool validateSmtMode(const std::string& value, + const std::string& locationPath, + std::string& errorMessage) + { + if (value.empty()) { - if (entry.empty()) - { - continue; - } - const auto resolved = resolvePathFromBase(configDir, entry); - config.files.emplace_back(resolved.string()); + return true; } - return true; + static const std::unordered_set allowed = { + "single", + "portfolio", + "cross-check", + "dual-consensus", + }; + if (allowed.find(value) != allowed.end()) + { + return true; + } + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [single, portfolio, cross-check, dual-consensus]"; + return false; } - [[nodiscard]] bool applyStackAnalyzerConfig(const json& section, - const std::filesystem::path& configDir, - ProgramConfig& config, - std::string& errorMessage) + [[nodiscard]] bool validateCompileIRFormat(const std::string& value, + const std::string& locationPath, + std::string& errorMessage) { - if (!section.is_object()) + if (value.empty()) { - errorMessage = "stack_analyzer config must be a JSON object."; - return false; + return true; } - std::string pathValue; - if (!readStringValue(section, "compile_commands", pathValue, errorMessage)) - { - return false; - } - if (!pathValue.empty()) + std::string lowered = toLower(value); + if (!lowered.empty() && lowered.front() == '.') { - config.global.compile_commands = resolvePathFromBase(configDir, pathValue).string(); + lowered.erase(lowered.begin()); } - pathValue.clear(); - if (!readStringValue(section, "resource_model", pathValue, errorMessage)) + if (lowered == "bc" || lowered == "ll") { - return false; + return true; } - if (!pathValue.empty()) + + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [bc, ll]"; + return false; + } + + [[nodiscard]] bool validateStackAnalyzerMode(const std::string& value, + const std::string& locationPath, + std::string& errorMessage) + { + if (value.empty()) { - config.global.resource_model = resolvePathFromBase(configDir, pathValue).string(); + return true; } - pathValue.clear(); - if (!readStringValue(section, "escape_model", pathValue, errorMessage)) + const std::string lowered = toLower(trimCopy(value)); + if (lowered == "ir" || lowered == "abi") { - return false; + return true; } - if (!pathValue.empty()) + + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [ir, abi]"; + return false; + } + + [[nodiscard]] bool validateOutputFormat(const std::string& value, + const std::string& locationPath, + std::string& errorMessage) + { + if (value.empty()) { - config.global.escape_model = resolvePathFromBase(configDir, pathValue).string(); + return true; } - pathValue.clear(); - if (!readStringValue(section, "buffer_model", pathValue, errorMessage)) + const std::string lowered = toLower(trimCopy(value)); + if (lowered == "human" || lowered == "json" || lowered == "sarif") { - return false; + return true; } - if (!pathValue.empty()) + + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [human, json, sarif]"; + return false; + } + + [[nodiscard]] bool validateJobsValue(const std::string& value, + const std::string& locationPath, + std::string& errorMessage) + { + if (value.empty()) { - config.global.buffer_model = resolvePathFromBase(configDir, pathValue).string(); + return true; } - if (!readBoolValue(section, "demangle", config.global.demangle, errorMessage)) + const std::string trimmed = trimCopy(value); + const std::string lowered = toLower(trimmed); + if (lowered == "auto") { - return false; + return true; } - if (!readBoolValue(section, "timing", config.global.timing, errorMessage)) + + if (trimmed.empty()) { + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [auto, positive integer]"; return false; } - if (!readBoolValue(section, "include_compdb_deps", config.global.include_compdb_deps, - errorMessage)) + + for (char ch : trimmed) { - return false; + if (!std::isdigit(static_cast(ch))) + { + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [auto, positive integer]"; + return false; + } } - if (!readBoolValue(section, "quiet", config.global.quiet, errorMessage)) + + uint64_t parsed = 0; + try { - return false; + parsed = std::stoull(trimmed); } - if (!readUint64Value(section, "stack_limit", config.global.stack_limit, errorMessage)) + catch (const std::exception&) { + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. Allowed values: [auto, positive integer]"; return false; } - std::string stringValue; - if (!readStringValueAny(section, {"analysis-profile", "analysis_profile"}, stringValue, - errorMessage)) + if (parsed == 0) { + errorMessage = "Invalid value '" + value + "' for '" + locationPath + + "'. jobs must be >= 1 or 'auto'."; return false; } - if (!stringValue.empty()) - { - config.global.analysis_profile = stringValue; - } - bool boolValue = false; - if (!readBoolLikeValueAny(section, {"smt"}, boolValue, errorMessage)) + return true; + } + + [[nodiscard]] bool applyInvokeValue(const json& value, ProgramConfig& config, + std::string& errorMessage, + const std::string& locationPath) + { + std::vector tools; + if (!parseStringList(value, tools, errorMessage, locationPath)) { 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)) + std::string normalizeError; + const auto normalized = normalizeAndValidateToolList(tools, normalizeError); + if (!normalizeError.empty()) { + errorMessage = locationPath + ": " + normalizeError; return false; } - if (!stringValue.empty()) + + config.global.specificTools = normalized; + config.global.hasInvokedSpecificTools = !normalized.empty(); + return true; + } + + [[nodiscard]] bool applyLegacyRootInvokeAndInput(const json& root, + const std::filesystem::path& configDir, + ProgramConfig& config, + std::string& errorMessage) + { + if (const auto it = root.find("invoke"); it != root.end() && !it->is_null()) { - config.global.smt_backend = stringValue; + if (!applyInvokeValue(*it, config, errorMessage, "invoke")) + { + return false; + } } - stringValue.clear(); - if (!readStringValueAny(section, {"smt-secondary-backend", "smt_secondary_backend"}, - stringValue, errorMessage)) + if (const auto it = root.find("input"); it != root.end() && !it->is_null()) { - return false; + if (it->is_object()) + { + errorMessage = "Expected string or array of strings for 'input'."; + return false; + } + std::vector entries; + if (!parseStringList(*it, entries, errorMessage, "input")) + { + return false; + } + assignInputFiles(entries, configDir, config, true); + } + + return true; + } + + [[nodiscard]] bool applyAnalysisSection(const json& section, ProgramConfig& config, + std::string& errorMessage) + { + if (!validateKnownKeys(section, + { + "static", + "dynamic", + "static_analysis", + "dynamic_analysis", + "invoke", + }, + "analysis", errorMessage)) + { + return false; + } + + bool boolValue = false; + bool hasValue = false; + if (!readOptionalBoolAny(section, {"static", "static_analysis"}, boolValue, + errorMessage, "analysis.static", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.hasStaticAnalysis = boolValue; + } + + if (!readOptionalBoolAny(section, {"dynamic", "dynamic_analysis"}, boolValue, + errorMessage, "analysis.dynamic", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.hasDynamicAnalysis = boolValue; + } + + if (const auto it = section.find("invoke"); it != section.end() && !it->is_null()) + { + if (!applyInvokeValue(*it, config, errorMessage, "analysis.invoke")) + { + return false; + } + } + + return true; + } + + [[nodiscard]] bool applyFilesSection(const json& section, + const std::filesystem::path& configDir, + ProgramConfig& config, std::string& errorMessage) + { + if (!validateKnownKeys(section, + { + "input", + "entry_points", + "compile_commands", + "include_compdb_deps", + }, + "files", errorMessage)) + { + return false; + } + + bool hasValue = false; + std::vector listValue; + if (!readOptionalStringListAny(section, {"input"}, listValue, errorMessage, + "files.input", hasValue)) + { + return false; + } + if (hasValue) + { + assignInputFiles(listValue, configDir, config, true); + } + + if (!readOptionalStringListAny(section, {"entry_points"}, listValue, errorMessage, + "files.entry_points", hasValue)) + { + return false; + } + if (hasValue) + { + (void)parseEntryPoints(listValue, config); + } + + std::string stringValue; + if (!readOptionalStringAny(section, {"compile_commands"}, stringValue, errorMessage, + "files.compile_commands", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.compile_commands = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + bool boolValue = false; + if (!readOptionalBoolAny(section, {"include_compdb_deps"}, boolValue, errorMessage, + "files.include_compdb_deps", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.include_compdb_deps = boolValue; + } + + return true; + } + + [[nodiscard]] bool applyOutputSection(const json& section, ProgramConfig& config, + std::string& errorMessage) + { + if (!validateKnownKeys(section, + { + "sarif_format", + "report_file", + "output_file", + "verbose", + "quiet", + "demangle", + }, + "output", errorMessage)) + { + return false; + } + + bool hasValue = false; + bool boolValue = false; + if (!readOptionalBoolAny(section, {"sarif_format"}, boolValue, errorMessage, + "output.sarif_format", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.hasSarifFormat = boolValue; + } + + if (!readOptionalBoolAny(section, {"verbose"}, boolValue, errorMessage, + "output.verbose", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.verbose = boolValue; + } + + if (!readOptionalBoolAny(section, {"quiet"}, boolValue, errorMessage, "output.quiet", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.quiet = boolValue; + } + + if (!readOptionalBoolAny(section, {"demangle"}, boolValue, errorMessage, + "output.demangle", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.demangle = boolValue; + } + + std::string stringValue; + if (!readOptionalStringAny(section, {"report_file"}, stringValue, errorMessage, + "output.report_file", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.report_file = stringValue; + } + + if (!readOptionalStringAny(section, {"output_file"}, stringValue, errorMessage, + "output.output_file", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.output_file = stringValue; + } + + return true; + } + + [[nodiscard]] bool applyRuntimeSection(const json& section, ProgramConfig& config, + std::string& errorMessage) + { + if (!validateKnownKeys(section, + { + "async", + "ipc", + "ipc_path", + }, + "runtime", errorMessage)) + { + return false; + } + + bool hasValue = false; + bool boolValue = false; + if (!readOptionalBoolAny(section, {"async"}, boolValue, errorMessage, "runtime.async", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.hasAsync = boolValue ? std::launch::async : std::launch::deferred; + } + + std::string stringValue; + if (!readOptionalStringAny(section, {"ipc"}, stringValue, errorMessage, "runtime.ipc", + hasValue)) + { + return false; + } + if (hasValue) + { + if (!validateIpcValue(stringValue, errorMessage, "runtime.ipc")) + { + return false; + } + config.global.ipc = stringValue; + } + + if (!readOptionalStringAny(section, {"ipc_path"}, stringValue, errorMessage, + "runtime.ipc_path", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.ipcPath = stringValue; + } + + return true; + } + + [[nodiscard]] bool applyServerSection(const json& section, ProgramConfig& config, + std::string& errorMessage) + { + if (!validateKnownKeys(section, + { + "host", + "port", + "shutdown_token", + "shutdown_timeout_ms", + }, + "server", errorMessage)) + { + return false; + } + + bool hasValue = false; + std::string stringValue; + if (!readOptionalStringAny(section, {"host"}, stringValue, errorMessage, "server.host", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.serverHost = stringValue; + } + + if (!readOptionalStringAny(section, {"shutdown_token"}, stringValue, errorMessage, + "server.shutdown_token", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.shutdownToken = stringValue; + } + + uint64_t uintValue = 0; + if (!readOptionalUint64Any(section, {"port"}, uintValue, errorMessage, "server.port", + hasValue)) + { + return false; + } + if (hasValue) + { + if (uintValue > static_cast(std::numeric_limits::max()) || + uintValue > 65535U) + { + errorMessage = "server.port must be between 0 and 65535."; + return false; + } + config.global.serverPort = static_cast(uintValue); + } + + if (!readOptionalUint64Any(section, {"shutdown_timeout_ms"}, uintValue, errorMessage, + "server.shutdown_timeout_ms", hasValue)) + { + return false; + } + if (hasValue) + { + if (uintValue > static_cast(std::numeric_limits::max())) + { + errorMessage = "server.shutdown_timeout_ms is too large."; + return false; + } + config.global.shutdownTimeoutMs = static_cast(uintValue); + } + + return true; + } + + [[nodiscard]] bool applyStackAnalyzerSection(const json& section, + const std::filesystem::path& configDir, + ProgramConfig& config, + std::string& errorMessage, + std::string_view location) + { + if (!validateKnownKeys(section, + { + "mode", + "output_format", + "output-format", + "format", + "config", + "print_effective_config", + "print-effective-config", + "extra_args", + "compile_commands", + "compile-commands", + "compdb", + "compile_args", + "compile-args", + "compile_arg", + "compile-arg", + "include_dirs", + "include-dirs", + "include_dir", + "include-dir", + "defines", + "define", + "resource_model", + "resource-model", + "escape_model", + "escape-model", + "buffer_model", + "buffer-model", + "demangle", + "verbose", + "timing", + "include_compdb_deps", + "include-compdb-deps", + "quiet", + "compdb_fast", + "compdb-fast", + "stack_limit", + "stack-limit", + "analysis-profile", + "analysis_profile", + "smt", + "smt-backend", + "smt_backend", + "smt-secondary-backend", + "smt_secondary_backend", + "smt-mode", + "smt_mode", + "smt-timeout-ms", + "smt_timeout_ms", + "smt-budget-nodes", + "smt_budget_nodes", + "smt-rules", + "smt_rules", + "entry_points", + "only_functions", + "only-functions", + "only_function", + "only-function", + "only_func", + "only-func", + "only_files", + "only-files", + "only_file", + "only-file", + "only_dirs", + "only-dirs", + "only_dir", + "only-dir", + "exclude_dirs", + "exclude-dirs", + "exclude_dir", + "exclude-dir", + "jobs", + "resource_cross_tu", + "resource-cross-tu", + "no_resource_cross_tu", + "no-resource-cross-tu", + "uninitialized_cross_tu", + "uninitialized-cross-tu", + "no_uninitialized_cross_tu", + "no-uninitialized-cross-tu", + "resource_summary_cache_dir", + "resource-summary-cache-dir", + "compile_ir_cache_dir", + "compile-ir-cache-dir", + "compile_ir_format", + "compile-ir-format", + "resource_summary_cache_memory_only", + "resource-summary-cache-memory-only", + "include_stl", + "include-stl", + "stl", + "base_dir", + "base-dir", + "dump_filter", + "dump-filter", + "dump_ir", + "dump-ir", + "warnings_only", + "warnings-only", + }, + location, errorMessage)) + { + return false; + } + + bool hasValue = false; + std::string stringValue; + if (!readOptionalStringAny(section, {"mode"}, stringValue, errorMessage, + std::string(location) + ".mode", hasValue)) + { + return false; + } + if (hasValue) + { + if (!validateStackAnalyzerMode(stringValue, std::string(location) + ".mode", + errorMessage)) + { + return false; + } + config.global.stack_analyzer_mode = stringValue; + } + + if (!readOptionalStringAny(section, {"output_format", "output-format", "format"}, + stringValue, errorMessage, + std::string(location) + ".output_format", hasValue)) + { + return false; + } + if (hasValue) + { + if (!validateOutputFormat(stringValue, std::string(location) + ".output_format", + errorMessage)) + { + return false; + } + config.global.stack_analyzer_output_format = stringValue; + } + + if (!readOptionalStringAny(section, {"config"}, stringValue, errorMessage, + std::string(location) + ".config", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_config = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + bool boolValue = false; + if (!readOptionalBoolAny(section, {"print_effective_config", "print-effective-config"}, + boolValue, errorMessage, + std::string(location) + ".print_effective_config", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_print_effective_config = boolValue; + } + + std::vector listValue; + if (!readOptionalStringListAny(section, {"extra_args"}, listValue, errorMessage, + std::string(location) + ".extra_args", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_extra_args = listValue; + } + + if (!readOptionalStringAny(section, {"compile_commands", "compile-commands", "compdb"}, + stringValue, errorMessage, + std::string(location) + ".compile_commands", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.compile_commands = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalStringListAny( + section, {"compile_args", "compile-args", "compile_arg", "compile-arg"}, + listValue, errorMessage, std::string(location) + ".compile_args", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_compile_args = listValue; + } + + if (!readOptionalStringListAny( + section, {"include_dirs", "include-dirs", "include_dir", "include-dir"}, + listValue, errorMessage, std::string(location) + ".include_dirs", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_include_dirs = listValue; + } + + if (!readOptionalStringListAny(section, {"defines", "define"}, listValue, errorMessage, + std::string(location) + ".defines", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_defines = listValue; + } + + if (!readOptionalStringAny(section, {"resource_model", "resource-model"}, stringValue, + errorMessage, std::string(location) + ".resource_model", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.resource_model = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalStringAny(section, {"escape_model", "escape-model"}, stringValue, + errorMessage, std::string(location) + ".escape_model", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.escape_model = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalStringAny(section, {"buffer_model", "buffer-model"}, stringValue, + errorMessage, std::string(location) + ".buffer_model", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.buffer_model = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalBoolAny(section, {"demangle"}, boolValue, errorMessage, + std::string(location) + ".demangle", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.demangle = boolValue; + } + + if (!readOptionalBoolAny(section, {"verbose"}, boolValue, errorMessage, + std::string(location) + ".verbose", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.verbose = boolValue; + } + + if (!readOptionalBoolAny(section, {"timing"}, boolValue, errorMessage, + std::string(location) + ".timing", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.timing = boolValue; + } + + if (!readOptionalBoolAny(section, {"include_compdb_deps", "include-compdb-deps"}, + boolValue, errorMessage, + std::string(location) + ".include_compdb_deps", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.include_compdb_deps = boolValue; + } + + if (!readOptionalBoolAny(section, {"compdb_fast", "compdb-fast"}, boolValue, + errorMessage, std::string(location) + ".compdb_fast", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_compdb_fast = boolValue; + } + + if (!readOptionalBoolAny(section, {"quiet"}, boolValue, errorMessage, + std::string(location) + ".quiet", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.quiet = boolValue; + } + + uint64_t uintValue = 0; + if (!readOptionalUint64Any(section, {"stack_limit", "stack-limit"}, uintValue, + errorMessage, std::string(location) + ".stack_limit", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_limit = uintValue; } - if (!stringValue.empty()) + + if (!readOptionalScalarAsStringAny(section, {"jobs"}, stringValue, errorMessage, + std::string(location) + ".jobs", hasValue)) + { + return false; + } + if (hasValue) + { + if (!validateJobsValue(stringValue, std::string(location) + ".jobs", errorMessage)) + { + return false; + } + config.global.stack_analyzer_jobs = trimCopy(stringValue); + } + + if (!readOptionalStringAny(section, {"analysis-profile", "analysis_profile"}, + stringValue, errorMessage, + std::string(location) + ".analysis_profile", hasValue)) + { + return false; + } + if (hasValue) + { + if (!validateAnalysisProfile( + stringValue, std::string(location) + ".analysis_profile", errorMessage)) + { + return false; + } + config.global.analysis_profile = stringValue; + } + + bool smtBool = false; + if (!readOptionalBoolLikeAny(section, {"smt"}, smtBool, errorMessage, + std::string(location) + ".smt", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.smt = smtBool ? "on" : "off"; + } + + if (!readOptionalStringAny(section, {"smt-backend", "smt_backend"}, stringValue, + errorMessage, std::string(location) + ".smt_backend", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.smt_backend = stringValue; + } + + if (!readOptionalStringAny(section, {"smt-secondary-backend", "smt_secondary_backend"}, + stringValue, errorMessage, + std::string(location) + ".smt_secondary_backend", hasValue)) + { + return false; + } + if (hasValue) { config.global.smt_secondary_backend = stringValue; } - stringValue.clear(); - if (!readStringValueAny(section, {"smt-mode", "smt_mode"}, stringValue, errorMessage)) + if (!readOptionalStringAny(section, {"smt-mode", "smt_mode"}, stringValue, errorMessage, + std::string(location) + ".smt_mode", hasValue)) { return false; } - if (!stringValue.empty()) + if (hasValue) { + if (!validateSmtMode(stringValue, std::string(location) + ".smt_mode", + errorMessage)) + { + return false; + } config.global.smt_mode = stringValue; } - uint64_t uint64Value = 0; - if (!readUint64ValueAny(section, {"smt-timeout-ms", "smt_timeout_ms"}, uint64Value, - errorMessage)) + if (!readOptionalUint64Any(section, {"smt-timeout-ms", "smt_timeout_ms"}, uintValue, + errorMessage, std::string(location) + ".smt_timeout_ms", + hasValue)) + { + return false; + } + if (hasValue) + { + if (uintValue > std::numeric_limits::max()) + { + errorMessage = std::string(location) + ".smt_timeout_ms is too large."; + return false; + } + config.global.smt_timeout_ms = static_cast(uintValue); + } + + if (!readOptionalUint64Any(section, {"smt-budget-nodes", "smt_budget_nodes"}, uintValue, + errorMessage, std::string(location) + ".smt_budget_nodes", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.smt_budget_nodes = uintValue; + } + + if (!readOptionalStringListAny(section, {"smt-rules", "smt_rules"}, listValue, + errorMessage, std::string(location) + ".smt_rules", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.smt_rules = listValue; + } + + if (!readOptionalStringListAny(section, + {"only_functions", "only-functions", "only_function", + "only-function", "only_func", "only-func"}, + listValue, errorMessage, + std::string(location) + ".only_functions", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_only_functions = listValue; + } + + if (!readOptionalStringListAny( + section, {"only_files", "only-files", "only_file", "only-file"}, listValue, + errorMessage, std::string(location) + ".only_files", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_only_files = listValue; + } + + if (!readOptionalStringListAny( + section, {"only_dirs", "only-dirs", "only_dir", "only-dir"}, listValue, + errorMessage, std::string(location) + ".only_dirs", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_only_dirs = listValue; + } + + if (!readOptionalStringListAny( + section, {"exclude_dirs", "exclude-dirs", "exclude_dir", "exclude-dir"}, + listValue, errorMessage, std::string(location) + ".exclude_dirs", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_exclude_dirs = listValue; + } + + if (!readOptionalStringListAny(section, {"entry_points"}, listValue, errorMessage, + std::string(location) + ".entry_points", hasValue)) + { + return false; + } + if (hasValue) + { + (void)parseEntryPoints(listValue, config); + config.global.stack_analyzer_only_functions = listValue; + } + + if (!readOptionalBoolAny(section, {"resource_cross_tu", "resource-cross-tu"}, boolValue, + errorMessage, std::string(location) + ".resource_cross_tu", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_resource_cross_tu = boolValue; + } + + if (!readOptionalBoolAny(section, {"no_resource_cross_tu", "no-resource-cross-tu"}, + boolValue, errorMessage, + std::string(location) + ".no_resource_cross_tu", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_resource_cross_tu = !boolValue; + } + + if (!readOptionalBoolAny(section, {"uninitialized_cross_tu", "uninitialized-cross-tu"}, + boolValue, errorMessage, + std::string(location) + ".uninitialized_cross_tu", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_uninitialized_cross_tu = boolValue; + } + + if (!readOptionalBoolAny( + section, {"no_uninitialized_cross_tu", "no-uninitialized-cross-tu"}, boolValue, + errorMessage, std::string(location) + ".no_uninitialized_cross_tu", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_uninitialized_cross_tu = !boolValue; + } + + if (!readOptionalStringAny( + section, {"resource_summary_cache_dir", "resource-summary-cache-dir"}, + stringValue, errorMessage, + std::string(location) + ".resource_summary_cache_dir", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_resource_summary_cache_dir = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalBoolAny( + section, + {"resource_summary_cache_memory_only", "resource-summary-cache-memory-only"}, + boolValue, errorMessage, + std::string(location) + ".resource_summary_cache_memory_only", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_resource_summary_cache_memory_only = boolValue; + } + + if (!readOptionalStringAny(section, {"compile_ir_cache_dir", "compile-ir-cache-dir"}, + stringValue, errorMessage, + std::string(location) + ".compile_ir_cache_dir", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_compile_ir_cache_dir = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalStringAny(section, {"compile_ir_format", "compile-ir-format"}, + stringValue, errorMessage, + std::string(location) + ".compile_ir_format", hasValue)) { return false; } - if (const auto* timeoutValue = - findFirstValueForKeys(section, {"smt-timeout-ms", "smt_timeout_ms"}); - timeoutValue != nullptr && !timeoutValue->is_null()) + if (hasValue) { - if (uint64Value > std::numeric_limits::max()) + if (!validateCompileIRFormat( + stringValue, std::string(location) + ".compile_ir_format", errorMessage)) { - errorMessage = "smt-timeout-ms is too large."; return false; } - config.global.smt_timeout_ms = static_cast(uint64Value); + config.global.stack_analyzer_compile_ir_format = stringValue; + } + + if (!readOptionalBoolAny(section, {"include_stl", "include-stl", "stl"}, boolValue, + errorMessage, std::string(location) + ".include_stl", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_include_stl = boolValue; + } + + if (!readOptionalStringAny(section, {"base_dir", "base-dir"}, stringValue, errorMessage, + std::string(location) + ".base_dir", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_base_dir = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); } - uint64Value = 0; - if (!readUint64ValueAny(section, {"smt-budget-nodes", "smt_budget_nodes"}, uint64Value, - errorMessage)) + if (!readOptionalBoolAny(section, {"dump_filter", "dump-filter"}, boolValue, + errorMessage, std::string(location) + ".dump_filter", + hasValue)) { return false; } - if (const auto* budgetValue = - findFirstValueForKeys(section, {"smt-budget-nodes", "smt_budget_nodes"}); - budgetValue != nullptr && !budgetValue->is_null()) + if (hasValue) + { + config.global.stack_analyzer_dump_filter = boolValue; + } + + if (!readOptionalStringAny(section, {"dump_ir", "dump-ir"}, stringValue, errorMessage, + std::string(location) + ".dump_ir", hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_dump_ir = + stringValue.empty() ? std::string() + : resolvePathFromBase(configDir, stringValue).string(); + } + + if (!readOptionalBoolAny(section, {"warnings_only", "warnings-only"}, boolValue, + errorMessage, std::string(location) + ".warnings_only", + hasValue)) + { + return false; + } + if (hasValue) + { + config.global.stack_analyzer_warnings_only = boolValue; + } + + return true; + } + + [[nodiscard]] const json* findStackAnalyzerSection(const json& root, + std::string& errorMessage) + { + const auto itDirect = root.find("stack_analyzer"); + if (itDirect != root.end() && !itDirect->is_null()) + { + if (!itDirect->is_object()) + { + errorMessage = "Expected object for 'stack_analyzer'."; + return nullptr; + } + return &(*itDirect); + } + + const auto itAlias = root.find("stack-analyzer"); + if (itAlias != root.end() && !itAlias->is_null()) + { + if (!itAlias->is_object()) + { + errorMessage = "Expected object for 'stack-analyzer'."; + return nullptr; + } + return &(*itAlias); + } + + const auto itTools = root.find("tools"); + if (itTools == root.end() || itTools->is_null()) + { + return nullptr; + } + if (!itTools->is_object()) + { + errorMessage = "Expected object for 'tools'."; + return nullptr; + } + + if (!validateKnownKeys(*itTools, {"ctrace_stack_analyzer", "stack_analyzer"}, "tools", + errorMessage)) + { + return nullptr; + } + + const auto itToolAnalyzer = itTools->find("ctrace_stack_analyzer"); + if (itToolAnalyzer != itTools->end() && !itToolAnalyzer->is_null()) + { + if (!itToolAnalyzer->is_object()) + { + errorMessage = "Expected object for 'tools.ctrace_stack_analyzer'."; + return nullptr; + } + return &(*itToolAnalyzer); + } + + const auto itToolLegacy = itTools->find("stack_analyzer"); + if (itToolLegacy != itTools->end() && !itToolLegacy->is_null()) { - config.global.smt_budget_nodes = uint64Value; + if (!itToolLegacy->is_object()) + { + errorMessage = "Expected object for 'tools.stack_analyzer'."; + return nullptr; + } + return &(*itToolLegacy); } - std::vector rules; - if (!readStringListValueAny(section, {"smt-rules", "smt_rules"}, rules, errorMessage, - "smt-rules")) + return nullptr; + } + + [[nodiscard]] bool applySchemaVersion(const json& root, std::string& errorMessage) + { + const auto it = root.find("schema_version"); + if (it == root.end() || it->is_null()) + { + return true; + } + if (!it->is_number_unsigned()) + { + errorMessage = "Expected unsigned integer for 'schema_version'."; + return false; + } + const auto version = it->get(); + if (version != kToolConfigSchemaVersion) { + errorMessage = "Unsupported schema_version '" + std::to_string(version) + + "'. Supported version: " + std::to_string(kToolConfigSchemaVersion) + + "."; return false; } - if (!rules.empty() || - (findFirstValueForKeys(section, {"smt-rules", "smt_rules"}) != nullptr)) + return true; + } + + [[nodiscard]] bool applyCanonicalSections(const json& root, + const std::filesystem::path& configDir, + ProgramConfig& config, std::string& errorMessage) + { + if (const auto it = root.find("analysis"); it != root.end() && !it->is_null()) + { + if (!it->is_object()) + { + errorMessage = "Expected object for 'analysis'."; + return false; + } + if (!applyAnalysisSection(*it, config, errorMessage)) + { + return false; + } + } + + if (const auto it = root.find("files"); it != root.end() && !it->is_null()) + { + if (!it->is_object()) + { + errorMessage = "Expected object for 'files'."; + return false; + } + if (!applyFilesSection(*it, configDir, config, errorMessage)) + { + return false; + } + } + + if (const auto it = root.find("output"); it != root.end() && !it->is_null()) + { + if (!it->is_object()) + { + errorMessage = "Expected object for 'output'."; + return false; + } + if (!applyOutputSection(*it, config, errorMessage)) + { + return false; + } + } + + if (const auto it = root.find("runtime"); it != root.end() && !it->is_null()) { - config.global.smt_rules = std::move(rules); + if (!it->is_object()) + { + errorMessage = "Expected object for 'runtime'."; + return false; + } + if (!applyRuntimeSection(*it, config, errorMessage)) + { + return false; + } } - if (const auto itEntry = section.find("entry_points"); - itEntry != section.end() && !itEntry->is_null()) + if (const auto it = root.find("server"); it != root.end() && !it->is_null()) { - std::vector points; - if (!parseStringList(*itEntry, points, errorMessage, "entry_points")) + if (!it->is_object()) + { + errorMessage = "Expected object for 'server'."; + return false; + } + if (!applyServerSection(*it, config, errorMessage)) { return false; } - config.global.entry_points = joinComma(points); } return true; @@ -552,26 +1794,56 @@ namespace ctrace return false; } - const std::filesystem::path configDir = path.parent_path(); - config.global.config_file = path.lexically_normal().string(); - - if (!applyInvoke(root, config, errorMessage)) + if (!validateKnownKeys(root, + { + "schema_version", + "analysis", + "files", + "output", + "runtime", + "server", + "stack_analyzer", + "stack-analyzer", + "invoke", + "input", + "tools", + }, + "root", errorMessage)) { return false; } - if (!applyInputFiles(root, configDir, config, errorMessage)) + + if (!applySchemaVersion(root, errorMessage)) { return false; } - if (const auto* analyzerSection = findStackAnalyzerConfigSection(root); + const std::filesystem::path configDir = std::filesystem::absolute(path).parent_path(); + config.global.config_file = path.lexically_normal().string(); + + if (const auto* analyzerSection = findStackAnalyzerSection(root, errorMessage); analyzerSection != nullptr) { - if (!applyStackAnalyzerConfig(*analyzerSection, configDir, config, errorMessage)) + if (!applyStackAnalyzerSection(*analyzerSection, configDir, config, errorMessage, + "stack_analyzer")) { return false; } } + else if (!errorMessage.empty()) + { + return false; + } + + if (!applyLegacyRootInvokeAndInput(root, configDir, config, errorMessage)) + { + return false; + } + + if (!applyCanonicalSections(root, configDir, config, errorMessage)) + { + return false; + } return true; } diff --git a/src/Process/Tools/StackAnalyzerToolImplementation.cpp b/src/Process/Tools/StackAnalyzerToolImplementation.cpp index 09a1c6e..c409280 100644 --- a/src/Process/Tools/StackAnalyzerToolImplementation.cpp +++ b/src/Process/Tools/StackAnalyzerToolImplementation.cpp @@ -5,12 +5,16 @@ #include #include #include +#include +#include #include #include #include #include #include +#include + #include #if !defined(_WIN32) @@ -21,6 +25,7 @@ namespace { constexpr std::string_view kStackAnalyzerModule = "stack_analyzer"; + using Json = nlohmann::json; struct AnalyzerArgBuildResult { @@ -103,11 +108,13 @@ namespace std::vector& args = result.args; std::vector& report = result.bridgeReport; - args.reserve(inputFiles.size() + 32); - report.reserve(32); + args.reserve(inputFiles.size() + 96); + report.reserve(96); - args.emplace_back("--mode=ir"); - appendBridgeDecision(report, "--mode=ir", true, "forced by coretrace integration"); + const std::string analyzerMode = + config.global.stack_analyzer_mode.empty() ? "ir" : config.global.stack_analyzer_mode; + args.emplace_back("--mode=" + analyzerMode); + appendBridgeDecision(report, "--mode", true, "value='" + analyzerMode + "'"); if (!config.global.config_file.empty()) { appendBridgeDecision(report, "config source", true, @@ -120,8 +127,25 @@ namespace "no --config provided to coretrace"); } - appendFlagOption(args, report, "--format=json", config.global.hasSarifFormat, - "coretrace --sarif-format enabled", "coretrace --sarif-format disabled"); + appendValueOption(args, report, "--config", config.global.stack_analyzer_config, + "empty stack_analyzer.config; analyzer internal config disabled"); + appendFlagOption(args, report, "--print-effective-config", + config.global.stack_analyzer_print_effective_config, + "stack_analyzer.print_effective_config enabled", + "stack_analyzer.print_effective_config disabled"); + + if (!config.global.stack_analyzer_output_format.empty()) + { + args.emplace_back("--format=" + config.global.stack_analyzer_output_format); + appendBridgeDecision(report, "--format", true, + "value='" + config.global.stack_analyzer_output_format + "'"); + } + else + { + appendFlagOption(args, report, "--format=json", config.global.hasSarifFormat, + "derived from coretrace --sarif-format", + "empty stack_analyzer.output_format and sarif disabled"); + } appendFlagOption(args, report, "--verbose", config.global.verbose, "coretrace --verbose enabled", "coretrace --verbose disabled"); appendFlagOption(args, report, "--demangle", config.global.demangle, @@ -131,13 +155,47 @@ namespace appendFlagOption(args, report, "--include-compdb-deps", config.global.include_compdb_deps, "coretrace --include-compdb-deps enabled", "coretrace --include-compdb-deps disabled"); + appendFlagOption(args, report, "--compdb-fast", config.global.stack_analyzer_compdb_fast, + "stack_analyzer.compdb_fast enabled", + "stack_analyzer.compdb_fast disabled"); + appendFlagOption(args, report, "--STL", config.global.stack_analyzer_include_stl, + "stack_analyzer.include_stl enabled", + "stack_analyzer.include_stl disabled"); + appendFlagOption( + args, report, "--warnings-only", config.global.stack_analyzer_warnings_only, + "stack_analyzer.warnings_only enabled", "stack_analyzer.warnings_only disabled"); + appendFlagOption(args, report, "--dump-filter", config.global.stack_analyzer_dump_filter, + "stack_analyzer.dump_filter enabled", + "stack_analyzer.dump_filter 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"); + if (!config.global.stack_analyzer_jobs.empty()) + { + appendValueOption(args, report, "--jobs", config.global.stack_analyzer_jobs, + "empty stack_analyzer.jobs"); + } + else + { + appendBridgeDecision(report, "--jobs", false, + "empty stack_analyzer.jobs; analyzer default kept"); + } + + appendValueOption(args, report, "--resource-summary-cache-dir", + config.global.stack_analyzer_resource_summary_cache_dir, + "empty stack_analyzer.resource_summary_cache_dir"); + appendValueOption(args, report, "--compile-ir-cache-dir", + config.global.stack_analyzer_compile_ir_cache_dir, + "empty stack_analyzer.compile_ir_cache_dir"); + appendValueOption(args, report, "--compile-ir-format", + config.global.stack_analyzer_compile_ir_format, + "empty stack_analyzer.compile_ir_format"); + appendValueOption(args, report, "--dump-ir", config.global.stack_analyzer_dump_ir, + "empty stack_analyzer.dump_ir"); + appendValueOption(args, report, "--base-dir", config.global.stack_analyzer_base_dir, + "empty stack_analyzer.base_dir"); 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, @@ -154,8 +212,143 @@ namespace appendValueOption(args, report, "--smt-mode", config.global.smt_mode, "empty in coretrace config; analyzer default mode"); + if (!config.global.stack_analyzer_only_files.empty()) + { + for (const auto& filter : config.global.stack_analyzer_only_files) + { + appendValueOption(args, report, "--only-file", filter, + "empty value in stack_analyzer.only_files"); + } + } + else + { + appendBridgeDecision(report, "--only-file", false, "empty stack_analyzer.only_files"); + } + + if (!config.global.stack_analyzer_only_dirs.empty()) + { + for (const auto& filter : config.global.stack_analyzer_only_dirs) + { + appendValueOption(args, report, "--only-dir", filter, + "empty value in stack_analyzer.only_dirs"); + } + } + else + { + appendBridgeDecision(report, "--only-dir", false, "empty stack_analyzer.only_dirs"); + } + + if (!config.global.stack_analyzer_exclude_dirs.empty()) + { + for (const auto& filter : config.global.stack_analyzer_exclude_dirs) + { + appendValueOption(args, report, "--exclude-dir", filter, + "empty value in stack_analyzer.exclude_dirs"); + } + } + else + { + appendBridgeDecision(report, "--exclude-dir", false, + "empty stack_analyzer.exclude_dirs"); + } + + if (!config.global.stack_analyzer_only_functions.empty()) + { + args.emplace_back("--only-func"); + args.emplace_back(joinCsv(config.global.stack_analyzer_only_functions)); + appendBridgeDecision(report, "--only-func", true, + "value='" + joinCsv(config.global.stack_analyzer_only_functions) + + "'"); + } + else + { + appendValueOption(args, report, "--only-function", config.global.entry_points, + "empty in coretrace config; no entry-point filter"); + } + + if (!config.global.stack_analyzer_include_dirs.empty()) + { + for (const auto& includeDir : config.global.stack_analyzer_include_dirs) + { + if (includeDir.empty()) + { + appendBridgeDecision(report, "-I", false, + "empty value in stack_analyzer.include_dirs"); + continue; + } + args.emplace_back("-I" + includeDir); + appendBridgeDecision(report, "-I", true, "value='" + includeDir + "'"); + } + } + else + { + appendBridgeDecision(report, "-I", false, "empty stack_analyzer.include_dirs"); + } + + if (!config.global.stack_analyzer_defines.empty()) + { + for (const auto& macroDef : config.global.stack_analyzer_defines) + { + if (macroDef.empty()) + { + appendBridgeDecision(report, "-D", false, + "empty value in stack_analyzer.defines"); + continue; + } + args.emplace_back("-D" + macroDef); + appendBridgeDecision(report, "-D", true, "value='" + macroDef + "'"); + } + } + else + { + appendBridgeDecision(report, "-D", false, "empty stack_analyzer.defines"); + } + + if (!config.global.stack_analyzer_compile_args.empty()) + { + for (const auto& compileArg : config.global.stack_analyzer_compile_args) + { + appendValueOption(args, report, "--compile-arg", compileArg, + "empty value in stack_analyzer.compile_args"); + } + } + else + { + appendBridgeDecision(report, "--compile-arg", false, + "empty stack_analyzer.compile_args"); + } + appendFlagOption(args, report, "--timing", config.global.timing, "coretrace timing enabled", "coretrace timing disabled"); + appendFlagOption(args, report, "--resource-summary-cache-memory-only", + config.global.stack_analyzer_resource_summary_cache_memory_only, + "stack_analyzer.resource_summary_cache_memory_only enabled", + "stack_analyzer.resource_summary_cache_memory_only disabled"); + if (config.global.stack_analyzer_resource_cross_tu.has_value()) + { + const bool enabled = *config.global.stack_analyzer_resource_cross_tu; + args.emplace_back(enabled ? "--resource-cross-tu" : "--no-resource-cross-tu"); + appendBridgeDecision(report, "resource_cross_tu", true, + enabled ? "enabled" : "disabled"); + } + else + { + appendBridgeDecision(report, "resource_cross_tu", false, + "not set; analyzer default kept"); + } + + if (config.global.stack_analyzer_uninitialized_cross_tu.has_value()) + { + const bool enabled = *config.global.stack_analyzer_uninitialized_cross_tu; + args.emplace_back(enabled ? "--uninitialized-cross-tu" : "--no-uninitialized-cross-tu"); + appendBridgeDecision(report, "uninitialized_cross_tu", true, + enabled ? "enabled" : "disabled"); + } + else + { + appendBridgeDecision(report, "uninitialized_cross_tu", false, + "not set; analyzer default kept"); + } if (config.global.stack_limit > 0) { args.emplace_back("--stack-limit"); @@ -205,6 +398,24 @@ namespace "empty in coretrace config; analyzer default rules"); } + if (!config.global.stack_analyzer_extra_args.empty()) + { + for (const auto& extraArg : config.global.stack_analyzer_extra_args) + { + if (extraArg.empty()) + { + continue; + } + args.emplace_back(extraArg); + appendBridgeDecision(report, "extra-arg", true, "value='" + extraArg + "'"); + } + } + else + { + appendBridgeDecision(report, "extra-args", false, + "no stack_analyzer.extra_args configured"); + } + for (const auto& file : inputFiles) { args.push_back(file); @@ -280,6 +491,108 @@ namespace return summary; } + [[nodiscard]] std::string toLowerAscii(std::string_view input) + { + std::string lowered; + lowered.reserve(input.size()); + for (const char ch : input) + { + lowered.push_back(static_cast(std::tolower(static_cast(ch)))); + } + return lowered; + } + + void accumulateSeverity(ctrace::DiagnosticSummary& summary, std::string_view severity) + { + const std::string lowered = toLowerAscii(severity); + if (lowered == "error") + { + ++summary.error; + return; + } + if (lowered == "warning" || lowered == "warn") + { + ++summary.warning; + return; + } + if (lowered == "info" || lowered == "information" || lowered == "note") + { + ++summary.info; + } + } + + [[nodiscard]] std::optional + parseDiagnosticsSummaryFromStructuredOutput(std::string_view text) + { + Json root = Json::parse(text, nullptr, false); + if (root.is_discarded() || !root.is_object()) + { + return std::nullopt; + } + + if (const auto diagnosticsIt = root.find("diagnostics"); + diagnosticsIt != root.end() && diagnosticsIt->is_array()) + { + ctrace::DiagnosticSummary summary{}; + for (const auto& diagnostic : *diagnosticsIt) + { + if (!diagnostic.is_object()) + { + continue; + } + const auto severityIt = diagnostic.find("severity"); + if (severityIt == diagnostic.end() || !severityIt->is_string()) + { + continue; + } + accumulateSeverity(summary, severityIt->get_ref()); + } + return summary; + } + + if (const auto runsIt = root.find("runs"); runsIt != root.end() && runsIt->is_array()) + { + ctrace::DiagnosticSummary summary{}; + bool hasSarifResults = false; + for (const auto& run : *runsIt) + { + if (!run.is_object()) + { + continue; + } + const auto resultsIt = run.find("results"); + if (resultsIt == run.end() || !resultsIt->is_array()) + { + continue; + } + for (const auto& result : *resultsIt) + { + if (!result.is_object()) + { + continue; + } + hasSarifResults = true; + const auto levelIt = result.find("level"); + if (levelIt != result.end() && levelIt->is_string()) + { + accumulateSeverity(summary, levelIt->get_ref()); + } + else + { + // SARIF defaults missing level to "warning". + ++summary.warning; + } + } + } + if (hasSarifResults) + { + return summary; + } + } + + return std::nullopt; + } + void captureToolOutputOnly(const std::string& stream, const std::string& message) { const auto* ctx = ctrace::Thread::Output::capture_context; @@ -326,6 +639,77 @@ namespace } } + [[nodiscard]] bool writeReportToFile(const std::string& reportPath, std::string_view content, + std::string& errorMessage) + { + errorMessage.clear(); + if (reportPath.empty()) + { + errorMessage = "report path is empty"; + return false; + } + + try + { + const std::filesystem::path targetPath(reportPath); + const auto parent = targetPath.parent_path(); + if (!parent.empty()) + { + std::error_code mkdirError; + std::filesystem::create_directories(parent, mkdirError); + if (mkdirError) + { + errorMessage = "failed to create report directory '" + parent.string() + + "': " + mkdirError.message(); + return false; + } + } + + std::ofstream out(targetPath, std::ios::binary | std::ios::trunc); + if (!out.is_open()) + { + errorMessage = "failed to open report file '" + targetPath.string() + "'"; + return false; + } + + out.write(content.data(), static_cast(content.size())); + if (!out.good()) + { + errorMessage = "failed to write report file '" + targetPath.string() + "'"; + return false; + } + } + catch (const std::exception& ex) + { + errorMessage = "failed to write report file '" + reportPath + "': " + ex.what(); + return false; + } + + return true; + } + + [[nodiscard]] std::string resolveStableReportPath(std::string_view reportPath) + { + if (reportPath.empty()) + { + return {}; + } + + try + { + std::filesystem::path path(reportPath); + if (path.is_relative()) + { + path = std::filesystem::current_path() / path; + } + return path.lexically_normal().string(); + } + catch (const std::exception&) + { + return std::string(reportPath); + } + } + #if !defined(_WIN32) struct CapturedStreams { @@ -518,6 +902,7 @@ namespace ctrace ctrace::ProgramConfig config) const { m_lastDiagnosticsSummary = {}; + const std::string stableReportPath = resolveStableReportPath(config.global.report_file); std::vector inputFiles; inputFiles.reserve(files.size()); @@ -627,11 +1012,23 @@ namespace ctrace { m_lastDiagnosticsSummary = *parsedSummary; } + else if (const auto parsedSummary = + parseDiagnosticsSummaryFromStructuredOutput(capturedStdout); + parsedSummary.has_value()) + { + m_lastDiagnosticsSummary = *parsedSummary; + } else if (const auto parsedSummary = parseDiagnosticsSummaryFromText(capturedStderr); parsedSummary.has_value()) { m_lastDiagnosticsSummary = *parsedSummary; } + else if (const auto parsedSummary = + parseDiagnosticsSummaryFromStructuredOutput(capturedStderr); + parsedSummary.has_value()) + { + m_lastDiagnosticsSummary = *parsedSummary; + } if (!runResult.isOk()) { @@ -645,6 +1042,23 @@ namespace ctrace std::to_string(runResult.exitCode)); return; } + + if (!stableReportPath.empty()) + { + std::string writeError; + if (!writeReportToFile(stableReportPath, capturedStdout, writeError)) + { + coretrace::log(coretrace::Level::Warn, coretrace::Module(kStackAnalyzerModule), + "Unable to persist stack analyzer report to '{}': {}\n", + stableReportPath, writeError); + } + else if (config.global.verbose) + { + coretrace::log(coretrace::Level::Debug, coretrace::Module(kStackAnalyzerModule), + "Stack analyzer report persisted to '{}' ({} bytes)\n", + stableReportPath, capturedStdout.size()); + } + } } std::string StackAnalyzerToolImplementation::name() const diff --git a/tests/config_parser_tests.cpp b/tests/config_parser_tests.cpp new file mode 100644 index 0000000..b7ffdbe --- /dev/null +++ b/tests/config_parser_tests.cpp @@ -0,0 +1,225 @@ +#include "App/Config.hpp" +#include "App/ToolConfig.hpp" + +#include +#include +#include +#include +#include +#include + +namespace +{ + std::filesystem::path makeTempConfigPath(const std::string& fileName) + { + const auto base = std::filesystem::temp_directory_path() / "ctrace-config-tests"; + std::error_code err; + std::filesystem::create_directories(base, err); + return base / fileName; + } + + void writeTextFile(const std::filesystem::path& path, const std::string& content) + { + std::ofstream out(path); + if (!out.is_open()) + { + std::cerr << "Failed to open file for writing: " << path << std::endl; + std::exit(1); + } + out << content; + } + + void testCanonicalConfigParsing() + { + const auto path = makeTempConfigPath("canonical.json"); + writeTextFile(path, R"json( +{ + "schema_version": 1, + "analysis": { + "static": true, + "dynamic": false, + "invoke": ["ctrace_stack_analyzer"] + }, + "files": { + "input": ["./tests/buffer_overflow.cc"], + "entry_points": ["main", "helper"], + "compile_commands": "", + "include_compdb_deps": true + }, + "output": { + "sarif_format": true, + "report_file": "cfg-report.txt", + "output_file": "cfg-output.txt", + "verbose": false, + "quiet": true, + "demangle": true + }, + "runtime": { + "async": false, + "ipc": "standardIO", + "ipc_path": "/tmp/coretrace-test-ipc" + }, + "server": { + "host": "127.0.0.1", + "port": 8081, + "shutdown_token": "token", + "shutdown_timeout_ms": 500 + }, + "stack_analyzer": { + "mode": "ir", + "output_format": "json", + "timing": true, + "analysis_profile": "full", + "smt": "on", + "smt_backend": "z3", + "smt_secondary_backend": "single", + "smt_mode": "single", + "smt_timeout_ms": 80, + "smt_budget_nodes": 1024, + "smt_rules": ["stack-buffer"], + "stack_limit": 4096, + "resource_model": "./resource.txt", + "escape_model": "./escape.txt", + "buffer_model": "./buffer.txt", + "extra_args": ["--foo=bar", "--baz"] + } +} +)json"); + + ctrace::ProgramConfig cfg; + std::string err; + const bool ok = ctrace::applyToolConfigFile(cfg, path.string(), err); + assert(ok); + assert(err.empty()); + assert(cfg.global.hasStaticAnalysis); + assert(!cfg.global.hasDynamicAnalysis); + assert(cfg.global.hasInvokedSpecificTools); + assert(cfg.global.specificTools.size() == 1); + assert(cfg.global.specificTools.front() == "ctrace_stack_analyzer"); + assert(cfg.global.hasSarifFormat); + assert(cfg.global.report_file == "cfg-report.txt"); + assert(cfg.global.output_file == "cfg-output.txt"); + assert(cfg.global.quiet); + assert(cfg.global.demangle); + assert(cfg.global.entry_points == "main,helper"); + assert(cfg.global.include_compdb_deps); + assert(cfg.global.stack_analyzer_mode == "ir"); + assert(cfg.global.stack_analyzer_output_format == "json"); + assert(cfg.global.timing); + assert(cfg.global.analysis_profile == "full"); + assert(cfg.global.smt == "on"); + assert(cfg.global.smt_timeout_ms == 80U); + assert(cfg.global.smt_budget_nodes == 1024U); + assert(cfg.global.stack_limit == 4096U); + assert(cfg.global.stack_analyzer_extra_args.size() == 2); + assert(!cfg.files.empty()); + } + + void testRejectsUnknownRootKey() + { + const auto path = makeTempConfigPath("unknown-root.json"); + writeTextFile(path, R"json( +{ + "schema_version": 1, + "unknown_key": true +} +)json"); + + ctrace::ProgramConfig cfg; + std::string err; + const bool ok = ctrace::applyToolConfigFile(cfg, path.string(), err); + assert(!ok); + assert(err.find("Unknown key 'unknown_key' in 'root'") != std::string::npos); + } + + void testRejectsInvalidStackAnalyzerMode() + { + const auto path = makeTempConfigPath("invalid-smt-mode.json"); + writeTextFile(path, R"json( +{ + "schema_version": 1, + "stack_analyzer": { + "smt_mode": "bad-mode" + } +} +)json"); + + ctrace::ProgramConfig cfg; + std::string err; + const bool ok = ctrace::applyToolConfigFile(cfg, path.string(), err); + assert(!ok); + assert(err.find("smt_mode") != std::string::npos); + } + + void testLegacyConfigCompatibility() + { + const auto path = makeTempConfigPath("legacy.json"); + writeTextFile(path, R"json( +{ + "invoke": ["ctrace_stack_analyzer"], + "input": ["./tests/buffer_overflow.cc"], + "stack_analyzer": { + "analysis-profile": "full", + "smt-timeout-ms": 90, + "entry_points": ["main"] + } +} +)json"); + + ctrace::ProgramConfig cfg; + std::string err; + const bool ok = ctrace::applyToolConfigFile(cfg, path.string(), err); + assert(ok); + assert(err.empty()); + assert(cfg.global.analysis_profile == "full"); + assert(cfg.global.smt_timeout_ms == 90U); + assert(cfg.global.entry_points == "main"); + assert(cfg.global.hasInvokedSpecificTools); + } + + void testCliOverridesConfig() + { + const auto path = makeTempConfigPath("precedence.json"); + writeTextFile(path, R"json( +{ + "schema_version": 1, + "output": { + "verbose": false, + "report_file": "from-config.txt", + "output_file": "from-config.out" + }, + "analysis": { + "invoke": ["ctrace_stack_analyzer"] + } +} +)json"); + + std::vector args = { + "ctrace", "--config", path.string(), "--verbose", + "--report-file", "from-cli.txt", "--output-file", "from-cli.out", + }; + std::vector argv; + argv.reserve(args.size()); + for (auto& arg : args) + { + argv.push_back(arg.data()); + } + + const ctrace::ProgramConfig cfg = + ctrace::buildConfig(static_cast(argv.size()), argv.data()); + assert(cfg.global.verbose); + assert(cfg.global.report_file == "from-cli.txt"); + assert(cfg.global.output_file == "from-cli.out"); + } +} // namespace + +int main() +{ + testCanonicalConfigParsing(); + testRejectsUnknownRootKey(); + testRejectsInvalidStackAnalyzerMode(); + testLegacyConfigCompatibility(); + testCliOverridesConfig(); + std::cout << "config_parser_tests: all checks passed" << std::endl; + return 0; +}