Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ //
++ // fuzz — targeted single-function fuzzer runner for Qt classes.
++ //
++ // Generates a specialized single-function fuzzer, compiles and caches it in
++ // a temporary directory, then runs it against specific or random input.
++ //
++ // Usage:
++ // fuzz -c <ClassName> -f <functionName> [options]
++ // fuzz --cleanup
++ //
++ // Options:
++ // -c <ClassName> Qt class to fuzz (required)
++ // -f <functionName> Function to fuzz (required)
++ // -t <seconds> Fuzz duration for random mode (default: 30)
++ // -n / --iterations N Fuzz a fixed number of iterations instead of / in addition to -t
++ // -d <data> Inline input: one argument value per line
++ // -i <file> File with input data (same format as -d)
++ // -b <file> Raw binary corpus file (may be repeated; no serialization)
++ // --submodule <path> Qt submodule root for class/function discovery
++ // --qt-prefix <path> Qt install prefix (auto-detected if omitted)
++ // --list-cached List all cached fuzzers and their build status
++ // --cleanup Remove all cached fuzzers from the temp directory
++ //
++ // -d and -i are mutually exclusive.
++ // -t and -n may be combined: the loop stops when either limit is reached.
++ // Input format: one argument value per line, with exactly
++ // (numParams × N) lines for N function calls.
++ // Example: a 2-param function called twice uses 4 lines.
++ // -b may be repeated and combined with -d / -i.
++ //
++ // Return codes:
++ // 0 Generated fuzzer ran for full -t seconds without crash.
++ // 1 Generated fuzzer processed specific -d/-i input without crash.
++ // 2 Generated fuzzer crashed.
++ // 3 Error (class/function not found, build failure, etc.; see stderr).
++
++ #include "codegen.h"
++ #include "modulemap.h"
++ #include "qtdetect.h"
++ #include "scanner.h"
++
++ #include <algorithm>
++ #include <cerrno>
++ #include <cstdint>
++ #include <cstdlib>
++ #include <cstring>
++ #include <filesystem>
++ #include <fstream>
++ #include <iostream>
++ #include <sstream>
++ #include <string>
++ #include <vector>
++
++ #ifdef _WIN32
++ # define WIN32_LEAN_AND_MEAN
++ # define NOMINMAX
++ # include <windows.h>
++ #else
++ # include <csignal>
++ # include <sys/wait.h>
++ # include <unistd.h>
++ #endif
++
++ namespace fs = std::filesystem;
++
++ // ---------------------------------------------------------------------------
++ // Exit codes (match what callers expect)
++ // ---------------------------------------------------------------------------
++ static constexpr int EC_TIMEOUT = 0; // ran to timeout without crash
++ static constexpr int EC_GRACEFUL = 1; // corpus processed without crash
++ static constexpr int EC_CRASH = 2; // crash
++ static constexpr int EC_ERROR = 3; // configuration / build error
++
++ // ---------------------------------------------------------------------------
++ // Helpers
++ // ---------------------------------------------------------------------------
++ static fs::path cacheRoot()
++ {
++ #ifdef _WIN32
++ const char *tmp = std::getenv("TEMP");
++ if (!tmp || !tmp[0])
++ tmp = std::getenv("TMP");
++ return fs::path(tmp ? tmp : "C:\\Temp") / "qtfuzz";
++ #else
++ const char *tmp = std::getenv("TMPDIR");
++ return fs::path(tmp ? tmp : "/tmp") / "qtfuzz";
++ #endif
++ }
++
++ // Convert MinGW/MSYS2 Unix-style paths (/c/Users/...) to Windows-style
++ // (C:/Users/...) so cmd.exe and cmake.exe can resolve them.
++ // On non-Windows or paths that don't match the pattern, returns the input unchanged.
++ static std::string nativePath(const std::string &p)
++ {
++ #ifdef _WIN32
++ if (p.size() >= 2 && p[0] == '/' && std::isalpha(static_cast<unsigned char>(p[1]))
++ && (p.size() == 2 || p[2] == '/')) {
++ std::string r;
++ r += static_cast<char>(std::toupper(static_cast<unsigned char>(p[1])));
++ r += ':';
++ r += (p.size() > 2 ? p.substr(2) : "/");
++ return r;
++ }
++ #endif
++ return p;
++ }
++
++ // Resolve the actual cmake prefix that contains Qt6Config.cmake.
++ // Handles both installed Qt (<prefix>/lib/cmake/Qt6/) and supermodule builds
++ // (<prefix>/qtbase/lib/cmake/Qt6/). Returns the prefix unchanged when
++ // Qt6Config.cmake cannot be found — cmake will then emit the usual error.
++ static std::string resolveQtPrefix(const std::string &prefix)
++ {
++ if (prefix.empty())
++ return prefix;
++ const fs::path qt6cmake = fs::path("lib") / "cmake" / "Qt6" / "Qt6Config.cmake";
++ if (fs::exists(fs::path(prefix) / qt6cmake))
++ return prefix;
++ const fs::path qtbaseSub = fs::path(prefix) / "qtbase";
++ if (fs::exists(qtbaseSub / qt6cmake))
++ return qtbaseSub.string();
++ return prefix;
++ }
++
++ // Replace any character that is not alphanumeric with '_'.
++ static std::string sanitize(const std::string &s)
++ {
++ std::string r;
++ r.reserve(s.size());
++ for (char c : s)
++ r += std::isalnum(static_cast<unsigned char>(c)) ? c : '_';
++ return r;
++ }
++
++ // Wrap a path in quotes for shell commands.
++ static std::string shellQuote(const std::string &s)
++ {
++ #ifdef _WIN32
++ return "\"" + s + "\"";
++ #else
++ std::string r = "'";
++ for (char c : s) {
++ if (c == '\'')
++ r += "'\\''";
++ else
++ r += c;
++ }
++ r += "'";
++ return r;
++ #endif
++ }
++
++ static void usage(const char *prog)
++ {
++ std::cerr << "Usage: " << prog << " -c <ClassName> -f <functionName> [options]\n"
++ << " " << prog << " --cleanup\n\n"
++ << "Options:\n"
++ << " -c <ClassName> Qt class to fuzz (required)\n"
++ << " -f <functionName> Function to fuzz (required)\n"
++ << " -t <seconds> Fuzz duration — random mode (default: 30)\n"
++ << " -n / --iterations <N> Stop after N iterations (may combine with -t)\n"
++ << " -d <data> Inline input: one argument value per line\n"
++ << " -i <file> File with input data (same format as -d)\n"
++ << " -b <file> Raw binary corpus (may repeat; bypasses serialization)\n"
++ << " --submodule <path> Qt submodule root for class/function discovery\n"
++ << " --qt-prefix <path> Qt install prefix (auto-detected if omitted)\n"
++ << " --list-cached List all cached fuzzers and their build status\n"
++ << " --cleanup Remove all cached fuzzers\n\n"
++ << "Return codes:\n"
++ << " 0 Fuzzer ran to the -t / -n limit without crash.\n"
++ << " 1 Fuzzer processed specific -d/-i input without crash.\n"
++ << " 2 Fuzzer crashed.\n"
++ << " 3 Error (class/function not found, build failure, etc.).\n";
++ }
++
++ // ---------------------------------------------------------------------------
++ // Type-to-FuzzData-expression mapper (mirrors fuzzExprForType in codegen.cpp)
++ // ---------------------------------------------------------------------------
++ static std::string normType(const std::string &raw)
++ {
++ std::string t;
++ bool lastSpace = true;
++ for (char c : raw) {
++ if (std::isspace(static_cast<unsigned char>(c))) {
++ if (!lastSpace) {
++ t += ' ';
++ lastSpace = true;
++ }
++ } else {
++ t += c;
++ lastSpace = false;
++ }
++ }
++ while (!t.empty() && t.back() == ' ')
++ t.pop_back();
++ return t;
++ }
++
++ static std::string fuzzExpr(const std::string &rawType, const std::string &className)
++ {
++ const std::string type = normType(rawType);
++
++ if (type == "bool")
++ return "static_cast<bool>(fd.nextBool())";
++ if (type == "int" || type == "qint32")
++ return "static_cast<int>(fd.nextInt())";
++ if (type == "uint" || type == "quint32" || type == "unsigned int")
++ return "static_cast<uint>(fd.nextUInt())";
++ if (type == "qint64" || type == "qlonglong" || type == "long long")
++ return "static_cast<" + type + ">(fd.nextInt64())";
++ if (type == "quint64" || type == "qulonglong" || type == "unsigned long long")
++ return "static_cast<" + type + ">(fd.nextUInt64())";
++ if (type == "double")
++ return "static_cast<double>(fd.nextDouble())";
++ if (type == "float")
++ return "static_cast<float>(fd.nextFloat())";
++ if (type == "qreal")
++ return "static_cast<qreal>(fd.nextDouble())";
++ if (type == "char")
++ return "static_cast<char>(fd.nextByte())";
++ if (type == "uchar" || type == "unsigned char")
++ return "static_cast<uchar>(fd.nextByte())";
++ if (type == "char32_t")
++ return "static_cast<char32_t>(fd.nextInt())";
++ if (type == "short" || type == "qint16")
++ return "static_cast<short>(fd.nextRaw<short>())";
++ if (type == "ushort" || type == "quint16")
++ return "static_cast<ushort>(fd.nextRaw<ushort>())";
++ if (type == "qsizetype")
++ return "static_cast<qsizetype>(fd.nextInt64())";
++ if (type == "qptrdiff")
++ return "static_cast<qptrdiff>(fd.nextInt64())";
++
++ if (type == "QString" || type == "const QString &" || type == "const QString&")
++ return "fd.nextQString()";
++ if (type == "QByteArray" || type == "const QByteArray &" || type == "const QByteArray&")
++ return "fd.nextQByteArray()";
++
++ if (type.find("std::chrono::") == 0)
++ return type + "(fd.nextInt64())";
++
++ if (type == "QChar")
++ return "QChar(fd.nextRaw<ushort>())";
++ if (type == "QPoint")
++ return "QPoint(fd.nextInt(), fd.nextInt())";
++ if (type == "QPointF")
++ return "QPointF(fd.nextDouble(), fd.nextDouble())";
++ if (type == "QSize")
++ return "QSize(fd.nextInt(), fd.nextInt())";
++ if (type == "QSizeF")
++ return "QSizeF(fd.nextDouble(), fd.nextDouble())";
++ if (type == "QRect")
++ return "QRect(fd.nextInt(), fd.nextInt(), fd.nextInt(), fd.nextInt())";
++ if (type == "QRectF")
++ return "QRectF(fd.nextDouble(), fd.nextDouble(), fd.nextDouble(), fd.nextDouble())";
++ if (type == "QUrl" || type == "const QUrl &" || type == "const QUrl&")
++ return "QUrl(fd.nextQString())";
++ if (type == "QColor" || type == "const QColor &" || type == "const QColor&")
++ return "QColor(fd.nextByte(), fd.nextByte(), fd.nextByte(), fd.nextByte())";
++ if (type == "QDeadlineTimer")
++ return "QDeadlineTimer(fd.nextInt64())";
++ if (type == "QAnyStringView" || type == "const QAnyStringView &")
++ return "QAnyStringView(fd.nextQString())";
++
++ // Pointer → nullptr
++ if (!type.empty() && type.back() == '*')
++ return "static_cast<" + type + ">(nullptr)";
++
++ // Reference → strip and default-construct a local (const refs bind to temporaries)
++ if (!type.empty() && type.back() == '&') {
++ std::string base = type;
++ while (!base.empty() && (base.back() == '&' || base.back() == ' '))
++ base.pop_back();
++ if (base.size() >= 6 && base.substr(0, 6) == "const ")
++ base = base.substr(6);
++ while (!base.empty() && base.back() == ' ')
++ base.pop_back();
++ return "/* ref */ " + base + "{}";
++ }
++
++ // Qt:: scoped enums / flags
++ if (type.size() >= 4 && type.substr(0, 4) == "Qt::")
++ return "static_cast<" + type + ">(fd.nextByte())";
++
++ // Fully qualified with :: (not std::) → enum cast
++ if (type.find("::") != std::string::npos && !(type.size() >= 5 && type.substr(0, 5) == "std::"))
++ return "static_cast<" + type + ">(fd.nextByte())";
++
++ // Single-word uppercase Qt types
++ if (!type.empty() && std::isupper(static_cast<unsigned char>(type[0]))
++ && type.find('<') == std::string::npos) {
++ if (type.size() >= 2 && type[0] == 'Q' && std::isupper(static_cast<unsigned char>(type[1])))
++ return type + "{}";
++ // Nested enum / typedef inside the fuzzed class
++ return "static_cast<" + className + "::" + type + ">(fd.nextByte())";
++ }
++
++ if (type.find("std::optional") != std::string::npos)
++ return "std::nullopt";
++ if (type.size() >= 5 && type.substr(0, 5) == "std::")
++ return type + "{}";
++
++ return "fd.nextInt()"; // fallback
++ }
++
++ // ---------------------------------------------------------------------------
++ // Binary serialization: text line → FuzzData byte sequence
++ //
++ // Must match exactly what the FuzzData methods read:
++ // nextBool / nextByte → 1 byte
++ // nextRaw<T> → sizeof(T) bytes, big-endian
++ // nextQString → uint16_t(len) + len bytes (len = uint16 % 257, max 256)
++ // nextQByteArray → uint16_t(len) + len bytes (len = uint16 % 513, max 512)
++ // nextDouble → uint64 big-endian reinterpreted as IEEE 754 double
++ // nextFloat → uint32 big-endian reinterpreted as IEEE 754 float
++ // ---------------------------------------------------------------------------
++ static void writeU64Be(uint64_t v, std::vector<uint8_t> &out)
++ {
++ for (int i = 7; i >= 0; --i)
++ out.push_back(static_cast<uint8_t>((v >> (i * 8)) & 0xFF));
++ }
++
++ static void writeU32Be(uint32_t v, std::vector<uint8_t> &out)
++ {
++ out.push_back((v >> 24) & 0xFF);
++ out.push_back((v >> 16) & 0xFF);
++ out.push_back((v >> 8) & 0xFF);
++ out.push_back(v & 0xFF);
++ }
++
++ // Returns false and emits a warning if the type cannot be serialized precisely.
++ static bool serializeParam(const std::string &rawType, const std::string &text,
++ std::vector<uint8_t> &out)
++ {
++ const std::string type = normType(rawType);
++
++ if (type == "bool") {
++ out.push_back((text == "true" || text == "1" || text == "yes") ? 1u : 0u);
++ return true;
++ }
++ if (type == "int" || type == "qint32") {
++ int32_t v = 0;
++ try {
++ v = static_cast<int32_t>(std::stol(text));
++ } catch (...) { }
++ writeU32Be(static_cast<uint32_t>(v), out);
++ return true;
++ }
++ if (type == "uint" || type == "quint32" || type == "unsigned int") {
++ uint32_t v = 0;
++ try {
++ v = static_cast<uint32_t>(std::stoul(text));
++ } catch (...) { }
++ writeU32Be(v, out);
++ return true;
++ }
++ if (type == "qint64" || type == "qlonglong" || type == "long long") {
++ int64_t v = 0;
++ try {
++ v = std::stoll(text);
++ } catch (...) { }
++ writeU64Be(static_cast<uint64_t>(v), out);
++ return true;
++ }
++ if (type == "quint64" || type == "qulonglong" || type == "unsigned long long") {
++ uint64_t v = 0;
++ try {
++ v = std::stoull(text);
++ } catch (...) { }
++ writeU64Be(v, out);
++ return true;
++ }
++ if (type == "double" || type == "qreal") {
++ double d = 0.0;
++ try {
++ d = std::stod(text);
++ } catch (...) { }
++ uint64_t bits = 0;
++ std::memcpy(&bits, &d, 8);
++ writeU64Be(bits, out);
++ return true;
++ }
++ if (type == "float") {
++ float f = 0.0f;
++ try {
++ f = std::stof(text);
++ } catch (...) { }
++ uint32_t bits = 0;
++ std::memcpy(&bits, &f, 4);
++ writeU32Be(bits, out);
++ return true;
++ }
++ if (type == "char" || type == "uchar" || type == "unsigned char") {
++ out.push_back(text.empty() ? 0u : static_cast<uint8_t>(text[0]));
++ return true;
++ }
++ if (type == "short" || type == "qint16") {
++ int16_t v = 0;
++ try {
++ v = static_cast<int16_t>(std::stoi(text));
++ } catch (...) { }
++ out.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
++ out.push_back(static_cast<uint8_t>(v & 0xFF));
++ return true;
++ }
++ if (type == "ushort" || type == "quint16") {
++ uint16_t v = 0;
++ try {
++ v = static_cast<uint16_t>(std::stoul(text));
++ } catch (...) { }
++ out.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
++ out.push_back(static_cast<uint8_t>(v & 0xFF));
++ return true;
++ }
++ // QString-like: nextQString reads uint16_t len then len bytes.
++ // With maxLen=256: len = uint16_val % 257, so len ≤ 256.
++ if (type == "QString" || type == "const QString &" || type == "const QString&" || type == "QUrl"
++ || type == "const QUrl &" || type == "const QUrl&" || type == "QAnyStringView"
++ || type == "const QAnyStringView &") {
++ size_t len = std::min(text.size(), size_t(256));
++ auto ul = static_cast<uint16_t>(len);
++ out.push_back(static_cast<uint8_t>((ul >> 8) & 0xFF));
++ out.push_back(static_cast<uint8_t>(ul & 0xFF));
++ out.insert(out.end(), text.begin(), text.begin() + static_cast<ptrdiff_t>(len));
++ return true;
++ }
++ // QByteArray: nextQByteArray reads uint16_t len then len bytes, maxLen=512.
++ if (type == "QByteArray" || type == "const QByteArray &" || type == "const QByteArray&") {
++ size_t len = std::min(text.size(), size_t(512));
++ auto ul = static_cast<uint16_t>(len);
++ out.push_back(static_cast<uint8_t>((ul >> 8) & 0xFF));
++ out.push_back(static_cast<uint8_t>(ul & 0xFF));
++ out.insert(out.end(), text.begin(), text.begin() + static_cast<ptrdiff_t>(len));
++ return true;
++ }
++ // QChar: nextRaw<ushort>() → 2 bytes
++ if (type == "QChar") {
++ uint16_t v = text.empty() ? 0u : static_cast<uint16_t>(static_cast<unsigned char>(text[0]));
++ if (text.size() > 1 && std::isdigit(static_cast<unsigned char>(text[0])))
++ try {
++ v = static_cast<uint16_t>(std::stoul(text));
++ } catch (...) { }
++ out.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
++ out.push_back(static_cast<uint8_t>(v & 0xFF));
++ return true;
++ }
++
++ // Unknown / complex type — write 4 zero bytes as best-effort nextInt() fallback.
++ std::cerr << "[fuzz] WARNING: no precise serializer for type '" << rawType
++ << "'; using 4 zero bytes.\n";
++ out.push_back(0);
++ out.push_back(0);
++ out.push_back(0);
++ out.push_back(0);
++ return false;
++ }
++
++ // ---------------------------------------------------------------------------
++ // Specialized fuzzer source generation
++ // ---------------------------------------------------------------------------
++ static const char kFuzzerTemplate[] = R"FUZZ(
++ // GENERATED by fuzz tool — do not edit by hand.
++ // Targeted fuzzer: @@CLASS@@::@@FUNC@@
++
++ // Prevent Windows SDK min/max macros from shadowing std::max.
++ #if defined(_WIN32) || defined(_WIN64)
++ # ifndef NOMINMAX
++ # define NOMINMAX
++ # endif
++ #endif
++
++ #include <@@CLASS@@>
++ #include @@APP_INCLUDE@@
++ #include <QString>
++ #include <QByteArray>
++ #include <QUrl>
++ #include <QPoint>
++ #include <QPointF>
++ #include <QSize>
++ #include <QSizeF>
++ #include <QRect>
++ #include <QRectF>
++ #if QT_GUI_LIB
++ #include <QColor>
++ #endif
++ #include <QChar>
++ #include <QDate>
++ #include <QTime>
++ #include <QDateTime>
++ #include <chrono>
++ #include <cstdint>
++ #include <cstdlib>
++ #include <cstring>
++ #include <fstream>
++ #include <iostream>
++ #include <iterator>
++ #include <vector>
++
++ @@FUZZ_DATA_STRUCT@@
++
++ static void fuzz_target(@@CLASS@@ &obj, FuzzData &fd)
++ {
++ @@FUNC_CALL@@
++ }
++
++ int main(int argc, char *argv[])
++ {
++ @@APP_CLASS@@ app(argc, argv);
++
++ int timeLimitSec = 0;
++ uint64_t maxIterations = 0;
++ bool corpusOnly = false;
++ std::vector<std::string> corpusFiles;
++
++ for (int i = 1; i < argc; i++) {
++ std::string a(argv[i]);
++ if ((a == "--time" || a == "-t") && i + 1 < argc)
++ timeLimitSec = std::atoi(argv[++i]);
++ else if ((a == "--iterations" || a == "-i") && i + 1 < argc) {
++ // Clamp negative values to 0 so they don't wrap to a huge uint64_t.
++ const long long n = std::atoll(argv[++i]);
++ maxIterations = n > 0 ? static_cast<uint64_t>(n) : 0;
++ }
++ else if (a == "--corpus-only")
++ corpusOnly = true;
++ else if (a.rfind("--", 0) != 0)
++ corpusFiles.push_back(a);
++ }
++
++ @@CLASS@@ obj;
++
++ for (const auto &path : corpusFiles) {
++ std::ifstream f(path, std::ios::binary);
++ if (!f) { std::cerr << "Cannot open corpus: " << path << "\n"; continue; }
++ std::vector<uint8_t> buf(std::istreambuf_iterator<char>(f), {});
++ FuzzData fd{ buf.data(), buf.size() };
++ fuzz_target(obj, fd);
++ }
++
++ if (corpusOnly)
++ return 1; // corpus processed without crash — exit code 1 = EC_GRACEFUL
++
++ uint64_t seed = 0xdeadbeefcafe1234ULL;
++ constexpr size_t BUF_SIZE = 4096;
++ std::vector<uint8_t> buf(BUF_SIZE);
++ uint64_t iterations = 0;
++ auto startTime = std::chrono::steady_clock::now();
++
++ for (;;) {
++ seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
++ uint64_t s = seed;
++ for (size_t i = 0; i < BUF_SIZE; i++) {
++ s ^= s << 13; s ^= s >> 7; s ^= s << 17;
++ buf[i] = static_cast<uint8_t>(s & 0xff);
++ }
++ FuzzData fd{ buf.data(), BUF_SIZE };
++ fuzz_target(obj, fd);
++ ++iterations;
++ if (maxIterations > 0 && iterations >= maxIterations)
++ break;
++ if (timeLimitSec <= 0 && maxIterations == 0)
++ break; // no limit specified: one iteration only
++ if (timeLimitSec > 0) {
++ auto elapsed = std::chrono::steady_clock::now() - startTime;
++ if (std::chrono::duration_cast<std::chrono::seconds>(elapsed).count() >= timeLimitSec)
++ break;
++ }
++ }
++
++ std::cout << "[fuzz_@@CLASS@@_@@FUNC@@] " << iterations << " iterations\n";
++ return 0; // timed out normally — exit code 0 = EC_TIMEOUT
++ }
++ )FUZZ";
++
++ // Build the call expression for fuzz_target(), handling non-const ref params
++ // by creating stack locals (can't bind a non-const ref to a temporary).
++ static std::string buildFuncCall(const std::string &funcName, const QtFuzz::MethodSignature &sig,
++ const std::string &className)
++ {
++ std::ostringstream o;
++
++ // Declare stack locals for non-const ref params.
++ for (size_t i = 0; i < sig.params.size(); ++i) {
++ if (!sig.params[i].isNonConstRef)
++ continue;
++ std::string base = sig.params[i].type;
++ while (!base.empty() && (base.back() == '&' || base.back() == ' '))
++ base.pop_back();
++ if (base.size() >= 6 && base.substr(0, 6) == "const ")
++ base = base.substr(6);
++ while (!base.empty() && base.back() == ' ')
++ base.pop_back();
++ o << base << " _p" << i << "{};\n ";
++ }
++
++ o << "(void)obj." << funcName << "(";
++ for (size_t i = 0; i < sig.params.size(); ++i) {
++ if (i > 0)
++ o << ", ";
++ o << (sig.params[i].isNonConstRef ? "_p" + std::to_string(i)
++ : fuzzExpr(sig.params[i].type, className));
++ }
++ o << ");";
++
++ // Suppress unused-parameter warning for zero-arg functions.
++ if (sig.params.empty())
++ o << "\n (void)fd;";
++
++ return o.str();
++ }
++
++ static std::string generateFuzzerSource(const std::string &className, const std::string &funcName,
++ const QtFuzz::MethodSignature &sig, QtFuzz::AppType appType)
++ {
++ const QtFuzz::AppTypeInfo ati = QtFuzz::appTypeInfo(appType);
++ const std::string funcCall = buildFuncCall(funcName, sig, className);
++
++ std::string src(kFuzzerTemplate);
++
++ auto replace = [](std::string &s, const std::string &from, const std::string &to) {
++ size_t pos = 0;
++ while ((pos = s.find(from, pos)) != std::string::npos) {
++ s.replace(pos, from.size(), to);
++ pos += to.size();
++ }
++ };
++
++ replace(src, "@@CLASS@@", className);
++ replace(src, "@@FUNC@@", funcName);
++ replace(src, "@@APP_INCLUDE@@", ati.includeHeader);
++ replace(src, "@@APP_CLASS@@", ati.className);
++ replace(src, "@@FUNC_CALL@@", funcCall);
++ replace(src, "@@FUZZ_DATA_STRUCT@@", QtFuzz::fuzzDataStructSource());
++
++ return src;
++ }
++
++ // ---------------------------------------------------------------------------
++ // Build helpers
++ // ---------------------------------------------------------------------------
++
++ // Run cmd (via shell), capture combined stdout+stderr.
++ // Returns empty string on success, captured output on failure.
++ static std::string runCommandCheck(const std::string &cmd)
++ {
++ #ifdef _WIN32
++ FILE *p = _popen((cmd + " 2>&1").c_str(), "r");
++ #else
++ FILE *p = popen((cmd + " 2>&1").c_str(), "r");
++ #endif
++ if (!p)
++ return "popen() failed for: " + cmd;
++
++ std::string out;
++ char buf[4096];
++ while (fgets(buf, sizeof(buf), p))
++ out += buf;
++
++ #ifdef _WIN32
++ const int code = _pclose(p);
++ if (code != 0)
++ return out.empty() ? "(no output)" : out;
++ #else
++ const int code = pclose(p);
++ if (code == -1 || WEXITSTATUS(code) != 0)
++ return out.empty() ? "(no output)" : out;
++ #endif
++ return { };
++ }
++
++ #ifdef _WIN32
++ // Locate vcvarsall.bat via vswhere.exe so cmake runs in the MSVC environment.
++ static std::string findVcVarsAll()
++ {
++ const std::string vswhere =
++ "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe";
++ if (!fs::exists(vswhere))
++ return { };
++ FILE *p = _popen(("\"" + vswhere + "\" -latest -property installationPath 2>nul").c_str(), "r");
++ if (!p)
++ return { };
++ char buf[512] = { };
++ const bool got = fgets(buf, sizeof(buf), p) != nullptr;
++ _pclose(p);
++ if (!got)
++ return { };
++ std::string path(buf);
++ while (!path.empty() && (path.back() == '\n' || path.back() == '\r' || path.back() == ' '))
++ path.pop_back();
++ return path.empty() ? std::string{ } : path + "\\VC\\Auxiliary\\Build\\vcvarsall.bat";
++ }
++ #endif
++
++ static std::string buildFuzzer(const fs::path &srcDir, const fs::path &buildDir,
++ const std::string &qtPrefix)
++ {
++ std::string configCmd =
++ "cmake -B " + shellQuote(buildDir.string()) + " -S " + shellQuote(srcDir.string());
++ if (!qtPrefix.empty())
++ configCmd += " -DCMAKE_PREFIX_PATH=" + shellQuote(qtPrefix);
++
++ #ifdef _WIN32
++ // Specify MSVC explicitly; without this cmake picks up whatever c++ is
++ // first in PATH (e.g. MinGW from Strawberry Perl).
++ configCmd += " -G Ninja -DCMAKE_CXX_COMPILER=cl -DCMAKE_C_COMPILER=cl";
++ // cmake must run inside the MSVC developer environment.
++ // Write a small batch file that initialises it then drives both cmake steps.
++ const fs::path batchFile = srcDir / "_build.bat";
++ {
++ std::ofstream bat(batchFile);
++ if (!bat)
++ return "Cannot write build script: " + batchFile.string();
++ const std::string vcvarsall = findVcVarsAll();
++ if (!vcvarsall.empty())
++ bat << "@call \"" << vcvarsall << "\" x64 >nul 2>&1\r\n";
++ // Delete stale CMakeCache.txt so a compiler change never causes a mismatch error.
++ bat << "@if exist " << shellQuote(buildDir.string() + "\\CMakeCache.txt") << " del /f /q "
++ << shellQuote(buildDir.string() + "\\CMakeCache.txt") << "\r\n";
++ bat << "@" << configCmd << "\r\n"
++ << "@if errorlevel 1 exit /b 1\r\n"
++ << "@cmake --build " << shellQuote(buildDir.string()) << "\r\n";
++ }
++ const std::string err = runCommandCheck("cmd /c " + shellQuote(batchFile.string()));
++ if (!err.empty())
++ return "build failed:\n" + err;
++ return { };
++ #else
++ std::string err = runCommandCheck(configCmd);
++ if (!err.empty())
++ return "cmake configure failed:\n" + err;
++
++ err = runCommandCheck("cmake --build " + shellQuote(buildDir.string()));
++ if (!err.empty())
++ return "cmake build failed:\n" + err;
++
++ return { };
++ #endif
++ }
++
++ // ---------------------------------------------------------------------------
++ // Subprocess runner
++ // ---------------------------------------------------------------------------
++ #ifdef _WIN32
++ static int runFuzzer(const fs::path &exe, const std::vector<std::string> &args, int timeoutSec,
++ const std::string &qtPrefix)
++ {
++ std::string cmdLine = "\"" + exe.string() + "\"";
++ for (const auto &a : args) {
++ cmdLine += ' ';
++ if (a.find(' ') != std::string::npos)
++ cmdLine += "\"" + a + "\"";
++ else
++ cmdLine += a;
++ }
++
++ // Prepend Qt's bin directory to PATH so the generated fuzzer can load Qt DLLs.
++ std::string envBlock;
++ if (!qtPrefix.empty()) {
++ const std::string qtBin = qtPrefix + "\\bin";
++ char currentPath[32768] = { };
++ GetEnvironmentVariableA("PATH", currentPath, sizeof(currentPath));
++ const std::string newPath = "PATH=" + qtBin + ";" + currentPath;
++ // Environment block: null-terminated strings, double-null at end.
++ envBlock = newPath;
++ envBlock.push_back('\0');
++ // Copy remaining environment variables.
++ const char *env = GetEnvironmentStringsA();
++ if (env) {
++ for (const char *p = env; *p; p += std::strlen(p) + 1) {
++ if (std::strncmp(p, "PATH=", 5) != 0 && std::strncmp(p, "Path=", 5) != 0
++ && std::strncmp(p, "path=", 5) != 0) {
++ envBlock.append(p, std::strlen(p) + 1);
++ }
++ }
++ FreeEnvironmentStringsA(const_cast<char *>(env));
++ }
++ envBlock.push_back('\0'); // double-null terminator
++ }
++
++ STARTUPINFOA si = { };
++ si.cb = sizeof(si);
++ PROCESS_INFORMATION pi = { };
++
++ const char *envPtr = envBlock.empty() ? nullptr : envBlock.data();
++ if (!CreateProcessA(nullptr, const_cast<char *>(cmdLine.c_str()), nullptr, nullptr, FALSE, 0,
++ const_cast<char *>(envPtr), nullptr, &si, &pi)) {
++ std::cerr << "[fuzz] CreateProcess failed with error " << GetLastError() << "\n";
++ return EC_ERROR;
++ }
++
++ const DWORD waitMs = static_cast<DWORD>((timeoutSec + 30) * 1000);
++ const DWORD result = WaitForSingleObject(pi.hProcess, waitMs);
++
++ int exitCode = EC_CRASH;
++ if (result == WAIT_OBJECT_0) {
++ DWORD code = 0;
++ if (GetExitCodeProcess(pi.hProcess, &code)) {
++ if (code == 0)
++ exitCode = EC_TIMEOUT;
++ else if (code == 1)
++ exitCode = EC_GRACEFUL;
++ }
++ } else {
++ std::cerr << "[fuzz] Fuzzer timed out (safety), terminating.\n";
++ TerminateProcess(pi.hProcess, 1);
++ }
++
++ CloseHandle(pi.hProcess);
++ CloseHandle(pi.hThread);
++ return exitCode;
++ }
++ #else
++ static volatile sig_atomic_t g_parentKilled = 0;
++ static pid_t g_childPid = -1;
++
++ static void onParentAlarm(int)
++ {
++ g_parentKilled = 1;
++ if (g_childPid > 0)
++ kill(g_childPid, SIGKILL);
++ }
++
++ static int runFuzzer(const fs::path &exe, const std::vector<std::string> &args, int timeoutSec)
++ {
++ const pid_t pid = fork();
++ if (pid < 0) {
++ std::cerr << "[fuzz] fork() failed: " << strerror(errno) << "\n";
++ return EC_ERROR;
++ }
++
++ if (pid == 0) {
++ // Child: exec the generated fuzzer.
++ std::vector<const char *> argv;
++ argv.push_back(exe.c_str());
++ for (const auto &a : args)
++ argv.push_back(a.c_str());
++ argv.push_back(nullptr);
++ execv(exe.c_str(), const_cast<char **>(argv.data()));
++ _exit(127);
++ }
++
++ // Parent: wait with a safety timeout so a hung child cannot block forever.
++ g_childPid = pid;
++ g_parentKilled = 0;
++ std::signal(SIGALRM, onParentAlarm);
++ alarm(static_cast<unsigned>(timeoutSec + 30));
++
++ int wstatus = 0;
++ waitpid(pid, &wstatus, 0);
++ alarm(0); // cancel parent alarm
++
++ if (WIFEXITED(wstatus)) {
++ const int code = WEXITSTATUS(wstatus);
++ if (code == 0)
++ return EC_TIMEOUT;
++ if (code == 1)
++ return EC_GRACEFUL;
++ return EC_CRASH;
++ }
++
++ if (WIFSIGNALED(wstatus)) {
++ const int sig = WTERMSIG(wstatus);
++ if (sig == SIGKILL && g_parentKilled)
++ std::cerr << "[fuzz] Fuzzer killed by parent safety timeout (hung).\n";
++ else
++ std::cerr << "[fuzz] Fuzzer killed by signal " << sig << ".\n";
++ return EC_CRASH;
++ }
++
++ return EC_CRASH;
++ }
++ #endif
++
++ // ---------------------------------------------------------------------------
++ // main
++ // ---------------------------------------------------------------------------
++ int main(int argc, char *argv[])
++ {
++ std::string className;
++ std::string funcName;
++ std::string inlineData;
++ std::string inputFile;
++ std::string submodule;
++ std::string qtPrefix;
++ std::vector<std::string> rawCorpusFiles;
++ int timeSec = 30;
++ bool timeset = false;
++ uint64_t maxIterations = 0;
++ bool doCleanup = false;
++ bool doListCached = false;
++
++ for (int i = 1; i < argc; i++) {
++ std::string a(argv[i]);
++ if (a == "--help" || a == "-h") {
++ usage(argv[0]);
++ return 0;
++ } else if (a == "--cleanup") {
++ doCleanup = true;
++ } else if (a == "--list-cached") {
++ doListCached = true;
++ } else if (a == "-c" && i + 1 < argc) {
++ className = argv[++i];
++ } else if (a == "-f" && i + 1 < argc) {
++ funcName = argv[++i];
++ } else if (a == "-t" && i + 1 < argc) {
++ timeSec = std::atoi(argv[++i]);
++ timeset = true;
++ } else if ((a == "-n" || a == "--iterations") && i + 1 < argc) {
++ const long long n = std::atoll(argv[++i]);
++ if (n <= 0) {
++ std::cerr << "[fuzz] ERROR: " << a << " requires a positive number, got '"
++ << argv[i] << "'.\n";
++ return EC_ERROR;
++ }
++ maxIterations = static_cast<uint64_t>(n);
++ } else if (a == "-d" && i + 1 < argc) {
++ inlineData = argv[++i];
++ } else if (a == "-i" && i + 1 < argc) {
++ inputFile = argv[++i];
++ } else if (a == "-b" && i + 1 < argc) {
++ rawCorpusFiles.push_back(argv[++i]);
++ } else if (a == "--submodule" && i + 1 < argc) {
++ submodule = argv[++i];
++ } else if (a == "--qt-prefix" && i + 1 < argc) {
++ qtPrefix = argv[++i];
++ }
++ }
++
++ // ── --list-cached ────────────────────────────────────────────────────────
++ if (doListCached) {
++ const fs::path root = cacheRoot();
++ std::error_code ec;
++ if (!fs::exists(root, ec) || ec) {
++ std::cout << "[fuzz] No cached fuzzers (cache dir does not exist).\n";
++ return 0;
++ }
++ bool any = false;
++ for (const auto &entry : fs::directory_iterator(root, ec)) {
++ if (ec) {
++ ec.clear();
++ continue;
++ }
++ if (!entry.is_directory())
++ continue;
++ const std::string name = entry.path().filename().string();
++ #ifdef _WIN32
++ const fs::path binary = entry.path() / "build" / ("fuzz_" + name + ".exe");
++ #else
++ const fs::path binary = entry.path() / "build" / ("fuzz_" + name);
++ #endif
++ std::error_code ec2;
++ const bool built = fs::exists(binary, ec2);
++ std::cout << name << (built ? " [built]" : " [source only]") << "\n";
++ any = true;
++ }
++ if (!any)
++ std::cout << "[fuzz] No cached fuzzers.\n";
++ return 0;
++ }
++
++ // ── --cleanup ────────────────────────────────────────────────────────────
++ if (doCleanup) {
++ std::error_code ec;
++ const auto n = fs::remove_all(cacheRoot(), ec);
++ std::cout << "[fuzz] Removed " << n << " entries from " << cacheRoot() << "\n";
++ return 0;
++ }
++
++ // ── Validate required args ───────────────────────────────────────────────
++ if (className.empty() || funcName.empty()) {
++ usage(argv[0]);
++ return EC_ERROR;
++ }
++ if (!inlineData.empty() && !inputFile.empty()) {
++ std::cerr << "[fuzz] ERROR: -d and -i are mutually exclusive.\n";
++ return EC_ERROR;
++ }
++ for (const auto &bf : rawCorpusFiles) {
++ if (!fs::exists(bf)) {
++ std::cerr << "[fuzz] ERROR: -b file not found: " << bf << "\n";
++ return EC_ERROR;
++ }
++ }
++ if (submodule.empty()) {
++ std::cerr << "[fuzz] ERROR: --submodule <path> is required.\n";
++ return EC_ERROR;
++ }
++
++ // ── Read input text ──────────────────────────────────────────────────────
++ std::string inputText;
++ if (!inlineData.empty()) {
++ inputText = inlineData;
++ } else if (!inputFile.empty()) {
++ std::ifstream f(inputFile);
++ if (!f) {
++ std::cerr << "[fuzz] ERROR: cannot open input file: " << inputFile << "\n";
++ return EC_ERROR;
++ }
++ std::ostringstream ss;
++ ss << f.rdbuf();
++ inputText = ss.str();
++ }
++
++ // ── Normalize paths (MinGW /c/... → C:/... for cmd.exe / cmake) ──────────
++ submodule = nativePath(submodule);
++ qtPrefix = nativePath(qtPrefix);
++
++ // ── Auto-detect Qt prefix ────────────────────────────────────────────────
++ if (qtPrefix.empty())
++ qtPrefix = QtFuzz::detectQtPrefix();
++ qtPrefix = resolveQtPrefix(qtPrefix);
++
++ // ── Look up class and function ───────────────────────────────────────────
++ auto classOpt = QtFuzz::scanSingleClass(className, fs::path(submodule));
++ if (!classOpt) {
++ std::cerr << "[fuzz] ERROR: class '" << className << "' not found under " << submodule
++ << ".\n";
++ return EC_ERROR;
++ }
++ const QtFuzz::DiscoveredClass &dc = *classOpt;
++
++ std::vector<const QtFuzz::MethodSignature *> matches;
++ for (const auto &sig : dc.publicMethods) {
++ if (sig.name == funcName)
++ matches.push_back(&sig);
++ }
++
++ if (matches.empty()) {
++ std::cerr << "[fuzz] ERROR: function '" << funcName << "' not found in class '" << className
++ << "'.\n"
++ << " Known public methods:\n";
++ for (const auto &sig : dc.publicMethods)
++ std::cerr << " " << sig.name << "\n";
++ return EC_ERROR;
++ }
++ if (matches.size() > 1) {
++ std::cerr << "[fuzz] NOTE: " << matches.size() << " overloads of '" << funcName
++ << "'; using the first one.\n";
++ }
++ const QtFuzz::MethodSignature &sig = *matches.front();
++ const size_t numParams = sig.params.size();
++
++ // ── Parse input lines ────────────────────────────────────────────────────
++ bool hasInput = !inputText.empty() || !rawCorpusFiles.empty();
++ std::vector<std::string> lines;
++ if (hasInput) {
++ std::istringstream ss(inputText);
++ std::string line;
++ while (std::getline(ss, line)) {
++ if (!line.empty() && line.back() == '\r')
++ line.pop_back();
++ lines.push_back(line);
++ }
++ while (!lines.empty() && lines.back().empty())
++ lines.pop_back();
++
++ if (numParams > 0 && lines.size() % numParams != 0) {
++ std::cerr << "[fuzz] ERROR: " << lines.size() << " input lines not divisible by "
++ << numParams << " (param count).\n";
++ return EC_ERROR;
++ }
++ }
++
++ // ── Cache setup ──────────────────────────────────────────────────────────
++ const std::string safeName = sanitize(className) + "_" + sanitize(funcName);
++ const std::string targetName = "fuzz_" + safeName;
++ const fs::path cDir = cacheRoot() / safeName;
++ const fs::path srcFile = cDir / (targetName + ".cpp");
++ const fs::path cmakeFile = cDir / "CMakeLists.txt";
++ const fs::path buildDir = cDir / "build";
++ #ifdef _WIN32
++ const fs::path binaryPath = buildDir / (targetName + ".exe");
++ #else
++ const fs::path binaryPath = buildDir / targetName;
++ #endif
++
++ // ── Generate and build (if not cached) ──────────────────────────────────
++ if (!fs::exists(binaryPath)) {
++ std::cerr << "[fuzz] Building cached fuzzer for " << className << "::" << funcName
++ << " ...\n";
++
++ std::error_code ec;
++ fs::create_directories(cDir, ec);
++ if (ec) {
++ std::cerr << "[fuzz] ERROR: cannot create cache dir: " << ec.message() << "\n";
++ return EC_ERROR;
++ }
++
++ // Write generated .cpp
++ {
++ std::ofstream f(srcFile);
++ if (!f) {
++ std::cerr << "[fuzz] ERROR: cannot write " << srcFile << "\n";
++ return EC_ERROR;
++ }
++ f << generateFuzzerSource(className, funcName, sig, dc.module.appType);
++ }
++
++ // Write CMakeLists.txt via existing CMakeGenerator.
++ // Use safeName as the cmake "className" so the target is fuzz_QDir_mkdir etc.
++ auto components = QtFuzz::resolveComponents(dc.module);
++ QtFuzz::CMakeGenerator::Config cfg{ safeName, targetName + ".cpp",
++ dc.module, components,
++ qtPrefix, timeSec };
++ QtFuzz::CMakeGenerator cmakeGen(std::move(cfg), cmakeFile);
++ if (!cmakeGen.generate()) {
++ std::cerr << "[fuzz] ERROR: cannot write CMakeLists.txt\n";
++ return EC_ERROR;
++ }
++
++ // Build
++ const std::string buildErr = buildFuzzer(cDir, buildDir, qtPrefix);
++ if (!buildErr.empty()) {
++ std::cerr << "[fuzz] ERROR: " << buildErr;
++ return EC_ERROR;
++ }
++ std::cerr << "[fuzz] Build complete: " << binaryPath << "\n";
++ } else {
++ std::cerr << "[fuzz] Using cached fuzzer: " << binaryPath << "\n";
++ }
++
++ // ── Serialize input to binary corpus files ───────────────────────────────
++ std::vector<std::string> corpusPaths;
++ if (!inputText.empty()) {
++ const size_t callCount = numParams > 0 ? lines.size() / numParams : 1;
++ for (size_t c = 0; c < callCount; ++c) {
++ std::vector<uint8_t> binary;
++ for (size_t p = 0; p < numParams; ++p)
++ serializeParam(sig.params[p].type, lines[c * numParams + p], binary);
++
++ const fs::path corpusFile = cDir / ("corpus_" + std::to_string(c) + ".bin");
++ std::ofstream f(corpusFile, std::ios::binary);
++ if (!f) {
++ std::cerr << "[fuzz] ERROR: cannot write corpus file " << corpusFile << "\n";
++ return EC_ERROR;
++ }
++ f.write(reinterpret_cast<const char *>(binary.data()),
++ static_cast<std::streamsize>(binary.size()));
++ corpusPaths.push_back(corpusFile.string());
++ }
++ }
++ // Raw binary corpus files: pass directly, no serialization.
++ for (const auto &bf : rawCorpusFiles)
++ corpusPaths.push_back(bf);
++
++ // ── Run ──────────────────────────────────────────────────────────────────
++ std::vector<std::string> fuzzArgs;
++ if (hasInput) {
++ // Corpus-only mode: process the specific inputs and exit.
++ fuzzArgs.push_back("--corpus-only");
++ for (const auto &p : corpusPaths)
++ fuzzArgs.push_back(p);
++ } else {
++ if (maxIterations > 0) {
++ fuzzArgs.push_back("--iterations");
++ fuzzArgs.push_back(std::to_string(maxIterations));
++ }
++ if (timeset || maxIterations == 0) {
++ fuzzArgs.push_back("--time");
++ fuzzArgs.push_back(std::to_string(timeSec));
++ }
++ }
++
++ // Safety timeout for the parent: when only iterations are specified (no -t),
++ // there is no meaningful time bound — use 24 h so the parent never kills a
++ // legitimately long run.
++ const int safetyTimeout = (!timeset && maxIterations > 0) ? 86400 : timeSec;
++
++ #ifdef _WIN32
++ return runFuzzer(binaryPath, fuzzArgs, safetyTimeout, qtPrefix);
++ #else
++ return runFuzzer(binaryPath, fuzzArgs, safetyTimeout);
++ #endif
++ }