// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include <QtCore/qprocessordetection.h>
#include <QtCore/qminmax.h>
#include <QtCore/qdebug.h>
#include <QtCore/qfile.h>
#include <QtCore/qdir.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/private/qcoreapplication_p.h>
#include <QtCore/private/qsystemerror_p.h>
#include <QtCore/private/qthread_p.h>
#include <QtCore/private/qcore_mac_p.h>
#include <QtCore/private/qiosrunloopintegration_p.h>
#include <QtCore/private/qeventdispatcher_cf_p.h>

#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSNotification.h>

#import <UIKit/UIApplication.h>
#import <UIKit/UISceneSession.h>

#if defined(Q_OS_VISIONOS)
extern "C" void runSwiftAppMain();
#endif

#include <setjmp.h> // Here be dragons

#include <sys/mman.h>

#define qAlignDown(val, align) val & ~(align - 1)
#define qAlignUp(val, align) qAlignDown(val + (align - 1), align)

static const size_t kBytesPerKiloByte = 1024;
static const long kPageSize = sysconf(_SC_PAGESIZE);

using namespace QT_PREPEND_NAMESPACE(QtPrivate);

/*
    The following diagram shows the layout of the reserved
    stack in relation to the regular stack, and the basic
    flow of the initial startup sequence. Note how we end
    up back in applicationDidLaunch after the user's main
    recurses into qApp-exec(), which allows us to return
    from applicationDidLaunch and spin the run-loop at the
    same level (UIApplicationMain) as iOS nativly does.

        +-----------------------------+
        |            qtmn()           |
        |     +--------------------+ <-- base
        | +---->      main()       |  |
        | |   +--------------------+  |
        | |   |        ...         |  |
        | |   +--------------------+  |
        | |   |    qApp->exec()    |  |
        | |   +--------------------+  |
        | |   |  processEvents()   |  |
        | |   |                    |  |
        | | +--+   longjmp(a)      |  |
        | | | |                    |  |
        | | | +--------------------+  |
        | | | |                    |  |
        | | | |                    |  |
        | | | |       unused       |  |
        | | | |                    |  |
        | | | |                    |  |
        | | | +--------------------+ <-- limit
        | | | |    memory guard    |  |
        | | | +--------------------+ <-- reservedStack
        +-|-|-------------------------+
        | | |  UIApplicationMain()    |
        +-|-|-------------------------+
        | | | applicationDidLaunch()  |
        | | |                         |
        | | +-->   setjmp(a)          |
        | +----+  trampoline()        |
        |                             |
        +-----------------------------+

    Note: the diagram does not reflect alignment issues.
*/

namespace
{
    rlimit stackLimit = {0, 0};
    rlim_t originalStackSize = 0;

    struct Stack
    {
        uintptr_t base;
        uintptr_t limit;

        static size_t computeSize(size_t requestedSize)
        {
            if (!requestedSize)
                return 0;

            // The stack size must be a multiple of 4 KB
            size_t stackSize = qAlignUp(requestedSize, 4 * kBytesPerKiloByte);

            // Be at least 16 KB
            stackSize = qMax(16 * kBytesPerKiloByte, stackSize);

            // Have enough extra space for our (aligned) memory guard
            stackSize += (2 * kPageSize);

            // But not exceed the 1MB maximum (adjusted to account for current stack usage)
            stackSize = qMin(stackSize, ((1024 - 64) * kBytesPerKiloByte));

            // Which we verify, just in case
            if (Q_UNLIKELY(stackSize > originalStackSize))
                qFatal("Unexpectedly exceeded stack limit");

            return stackSize;
        }

        void adopt(void* memory, size_t size)
        {
            uintptr_t memoryStart = uintptr_t(memory);

            // Add memory guard at the end of the reserved stack, so that any stack
            // overflow during the user's main will trigger an exception at that point,
            // and not when we return and find that the current stack has been smashed.
            // We allow read though, so that garbage-collection can pass through our
            // stack in its mark phase without triggering access violations.
            uintptr_t memoryGuardStart = qAlignUp(memoryStart, kPageSize);
            if (mprotect((void*)memoryGuardStart, kPageSize, PROT_READ))
                qWarning() << "Failed to add memory guard:" << strerror(errno);

            // We don't consider the memory guard part of the usable stack space
            limit = memoryGuardStart + kPageSize;

            // The stack grows downwards in memory, so the stack base is actually
            // at the end of the reserved stack space. And, as the memory guard
            // was page aligned, we need to align down the base as well, to
            // keep the requirement that the stack size is a multiple of 4K.
            base = qAlignDown(memoryStart + size, kPageSize);
        }

        bool isValid()
        {
            return base && limit;
        }

        size_t size()
        {
            return base - limit;
        }

        static const int kScribblePattern;

        void scribble()
        {
            memset_pattern4((void*)limit, &kScribblePattern, size());
        }

        void printUsage()
        {
            uintptr_t highWaterMark = limit;
            for (; highWaterMark < base; highWaterMark += 4) {
                if (memcmp((void*)highWaterMark, &kScribblePattern, 4))
                    break;
            }

            qDebug("main() used roughly %lu bytes of stack space", (base - highWaterMark));
        }
    };

    const int Stack::kScribblePattern = 0xfafafafa;

    Stack userMainStack;

    jmp_buf processEventEnterJumpPoint;
    jmp_buf processEventExitJumpPoint;

    bool applicationAboutToTerminate = false;
    jmp_buf applicationWillTerminateJumpPoint;

    bool debugStackUsage = false;

    struct {
        QAppleLogActivity UIApplicationMain;
        QAppleLogActivity applicationDidFinishLaunching;
    } logActivity;

    void updateStackLimit()
    {
        qCDebug(lcEventDispatcher) << "Updating RLIMIT_STACK soft limit from"
            << originalStackSize << "to" << userMainStack.size();

        stackLimit.rlim_cur = userMainStack.size();
        if (setrlimit(RLIMIT_STACK, &stackLimit) != 0) {
            qCWarning(lcEventDispatcher) << "Failed to update RLIMIT_STACK soft limit"
                                         << QSystemError::stdString();
        }
    }

    void restoreStackLimit()
    {
        qCDebug(lcEventDispatcher) << "Restoring RLIMIT_STACK soft limit from"
            << stackLimit.rlim_cur << "back to" << originalStackSize;

        stackLimit.rlim_cur = originalStackSize;
        if (setrlimit(RLIMIT_STACK, &stackLimit) != 0) {
            qCWarning(lcEventDispatcher) << "Failed to update RLIMIT_STACK soft limit"
                                         << QSystemError::stdString();
        }
    }
}

static int infoPlistValue(NSString* key, int defaultValue)
{
    static NSBundle *bundle = [NSBundle mainBundle];
    NSNumber* value = [bundle objectForInfoDictionaryKey:key];
    return value ? [value intValue] : defaultValue;
}

/*
    We replace the application delegate with our QIOSApplicationDelegate
    later on, but we need a placeholder delegate that implements dynamic
    scene management, otherwise iOS will mistakenly complain that we
    haven't adopted the UIScene lifecycle yet.
*/
@interface QIOSDynamicSceneDelegate : UIResponder <UIApplicationDelegate>
@end

@implementation QIOSDynamicSceneDelegate
- (UISceneConfiguration *)application:(UIApplication *)application
                          configurationForConnectingSceneSession:(UISceneSession *)session
                          options:(UISceneConnectionOptions *)options
{
    // If we're connecting a session that has its configuration in
    // the Info.plist we just want to pass on the static config.
    return session.configuration;
}
@end

extern "C" int qt_main_wrapper(int argc, char *argv[])
{
    @autoreleasepool {
        if (Q_UNLIKELY(getrlimit(RLIMIT_STACK, &stackLimit) != 0))
            qFatal("Failed to get stack limits");

        originalStackSize = stackLimit.rlim_cur;

        size_t defaultStackSize = 512 * kBytesPerKiloByte; // Same as secondary threads

        uint requestedStackSize = qMax(0, infoPlistValue(@"QtRunLoopIntegrationStackSize", defaultStackSize));

        if (infoPlistValue(@"QtRunLoopIntegrationDisableSeparateStack", false))
            requestedStackSize = 0;

        QT_WARNING_PUSH
#if Q_CC_CLANG >= 1800
        QT_WARNING_DISABLE_CLANG("-Wvla-cxx-extension")
#endif
        // The user-main stack _must_ live on the stack, so that the stack pointer
        // during user-main is within pthread_get_stackaddr_np/pthread_get_stacksize_np.
        char reservedStack[Stack::computeSize(requestedStackSize)];
        QT_WARNING_POP

        if (sizeof(reservedStack) > 0) {
            userMainStack.adopt(reservedStack, sizeof(reservedStack));

            if (infoPlistValue(@"QtRunLoopIntegrationDebugStackUsage", false)) {
                debugStackUsage = true;
                userMainStack.scribble();
                qDebug("Effective stack size is %lu bytes", userMainStack.size());
            }
        }

        logActivity.UIApplicationMain = QT_APPLE_LOG_ACTIVITY(
            lcEventDispatcher().isDebugEnabled(), "UIApplicationMain").enter();

#if defined(Q_OS_VISIONOS)
        Q_UNUSED(argc);
        Q_UNUSED(argv);
        qCDebug(lcEventDispatcher) << "Starting Swift app";
        runSwiftAppMain();
        Q_UNREACHABLE();
#else
        qCDebug(lcEventDispatcher) << "Running UIApplicationMain";
        return UIApplicationMain(argc, argv, nil,
            NSStringFromClass(QIOSDynamicSceneDelegate.class));
#endif
    }
}

enum SetJumpResult
{
    kJumpPointSetSuccessfully = 0,
    kJumpedFromEventDispatcherProcessEvents,
    kJumpedFromEventLoopExecInterrupt,
    kJumpedFromUserMainTrampoline,
};

static void __attribute__((noinline, noreturn)) user_main_trampoline()
{
    NSArray<NSString *> *arguments = [[NSProcessInfo processInfo] arguments];
    int argc = arguments.count;
    char **argv = new char*[argc];

    for (int i = 0; i < argc; ++i) {
        NSString *arg = [arguments objectAtIndex:i];

        NSStringEncoding cStringEncoding = [NSString defaultCStringEncoding];
        unsigned int bufferSize = [arg lengthOfBytesUsingEncoding:cStringEncoding] + 1;
        argv[i] = reinterpret_cast<char *>(malloc(bufferSize));

        if (Q_UNLIKELY(![arg getCString:argv[i] maxLength:bufferSize encoding:cStringEncoding]))
            qFatal("Could not convert argv[%d] to C string", i);
    }

    updateStackLimit();

    static const bool isRunningViaTestRunner =
        qEnvironmentVariableIsSet("QT_RUNNING_VIA_TEST_RUNNER");

    if (isRunningViaTestRunner) {
        qCDebug(lcEventDispatcher) << "Overriding working directory for test runner";
        QDir::temp().mkdir("testrunner");
        QDir::setCurrent(QDir::temp().filePath("testrunner"));
    } else {
        // Set current directory to app bundle folder for the convenience of
        // examples that load resources relative to the deployment path.
        // FIXME: This is akward, we should use the home dir or container
        QDir::setCurrent(QString::fromNSString(NSBundle.mainBundle.bundlePath));
    }

    int main(int argc, char *argv[]);

    int exitCode = main(argc, argv);
    delete[] argv;

    logActivity.applicationDidFinishLaunching.enter();
    qCDebug(lcEventDispatcher) << "Returned from main with exit code " << exitCode;

    if (isRunningViaTestRunner) {
        // When running via `simctl launch` or `devicectl device process launch`
        // the exit code is not always propagated, so we manually save it to disk.
        QFile exitCodeFile(QDir::tempPath() + "/qt_exit_code.txt");
        if (exitCodeFile.open(QIODevice::WriteOnly | QIODevice::Text))
            exitCodeFile.write(QByteArray::number(exitCode));
        else
            qCWarning(lcEventDispatcher) << "Failed to write exit code to" << exitCodeFile.fileName();
    }

    if (Q_UNLIKELY(debugStackUsage))
        userMainStack.printUsage();

    restoreStackLimit();

    logActivity.applicationDidFinishLaunching.leave();

    if (applicationAboutToTerminate)
        longjmp(applicationWillTerminateJumpPoint, kJumpedFromUserMainTrampoline);

    // We end up here if the user's main() never calls QApplication::exec(),
    // or if we return from exec() after quitting the application. If so we
    // follow the expected behavior from the point of the user's main(), which
    // is to exit with the given exit code.
    exit(exitCode);
}

void registerApplicationLifecycleCallbacks()
{
    [[NSNotificationCenter defaultCenter]
        addObserver:UIApplication.class
        selector:@selector(qt_applicationDidFinishLaunching:)
        name:UIApplicationDidFinishLaunchingNotification
        object:nil];

    [[NSNotificationCenter defaultCenter]
        addObserver:UIApplication.class
        selector:@selector(qt_applicationWillTerminate)
        name:UIApplicationWillTerminateNotification
        object:nil];
}
Q_CONSTRUCTOR_FUNCTION(registerApplicationLifecycleCallbacks)

@implementation UIApplication (QtRunLoopIntegration)

+ (bool)qt_rootLevelRunLoopIntegration
{
    // If we don't have a stack set up, we're not running inside
    // iOS' native/root level run-loop in UIApplicationMain.
    return userMainStack.isValid();
}

+ (void)qt_applicationDidFinishLaunching:(NSNotification *)notification
{
    logActivity.applicationDidFinishLaunching = QT_APPLE_LOG_ACTIVITY_WITH_PARENT(
        lcEventDispatcher().isDebugEnabled(), "applicationDidFinishLaunching", logActivity.UIApplicationMain).enter();

    qCDebug(lcEventDispatcher) << "Application launched with options" << notification.userInfo;

    if (![self qt_rootLevelRunLoopIntegration]) {
        // We schedule the main-redirection for the next run-loop pass, so that we
        // can return from this function and let UIApplicationMain finish its job.
        // This results in running Qt's application eventloop as a nested runloop.
        qCDebug(lcEventDispatcher) << "Scheduling main() on next run-loop pass";
        CFRunLoopTimerRef userMainTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault,
             CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef) { user_main_trampoline(); });
        CFRunLoopAddTimer(CFRunLoopGetMain(), userMainTimer, kCFRunLoopCommonModes);
        CFRelease(userMainTimer);
        return;
    }

    switch (setjmp(processEventEnterJumpPoint)) {
    case kJumpPointSetSuccessfully: {
        qCDebug(lcEventDispatcher) << "Running main() on separate stack";
        QT_APPLE_SCOPED_LOG_ACTIVITY(lcEventDispatcher().isDebugEnabled(), "main()");

#if defined(Q_PROCESSOR_X86)
#  define FUNCTION_CALL_ALIGNMENT 16
#  if defined(Q_PROCESSOR_X86_32)
#    define SET_STACK_POINTER "mov %0, %%esp"
#  elif defined(Q_PROCESSOR_X86_64)
#    define SET_STACK_POINTER "movq %0, %%rsp"
#  endif
#elif defined(Q_PROCESSOR_ARM)
#  // Valid for both 32 and 64-bit ARM
#  define FUNCTION_CALL_ALIGNMENT 4
#  define SET_STACK_POINTER "mov sp, %0"
#else
#  error "Unknown processor family"
#endif

        // Redirect the stack pointer to the start of the reserved stack. This ensures
        // that when we longjmp out of the event dispatcher and continue execution, the
        // 'Qt main' call-stack will not be smashed, as it lives in a part of the stack
        // that was allocated back in main().
        __asm__ __volatile__(
            SET_STACK_POINTER
            : /* no outputs */
            : "r" (qAlignDown(userMainStack.base, FUNCTION_CALL_ALIGNMENT))
        );

        user_main_trampoline();

        Q_UNREACHABLE();
        break;
    }
    case kJumpedFromEventDispatcherProcessEvents:
        // We've returned from the longjmp in the event dispatcher,
        // and the stack has been restored to its old self.
        logActivity.UIApplicationMain.enter();
        qCDebug(lcEventDispatcher) << "↳ Jumped from processEvents due to exec";

        if (Q_UNLIKELY(debugStackUsage))
            userMainStack.printUsage();

        restoreStackLimit();

        break;
    default:
        qFatal("Unexpected jump result in event loop integration");
    }
}

+ (bool)qt_eventDispatcherEnteredProcessEvents
{
    // We set a new jump point here that we can return to when the event loop
    // is asked to exit, so that we can return from QEventLoop::exec().
    switch (setjmp(processEventExitJumpPoint)) {
    case kJumpPointSetSuccessfully:
        qCDebug(lcEventDispatcher) << "QEventLoop exec detected, jumping back to system runloop ↵";
        longjmp(processEventEnterJumpPoint, kJumpedFromEventDispatcherProcessEvents);
        break;
    case kJumpedFromEventLoopExecInterrupt:
        // The event loop has exited (either by the hand of the user, or the iOS termination
        // signal), and we jumped back though processEventExitJumpPoint. We return from processEvents,
        // which will emit aboutToQuit if it's QApplication's event loop, and then return to the user's
        // main, which can do whatever it wants, including calling exec() on the application again.
        qCDebug(lcEventDispatcher) << "⇢ System runloop exited, returning with eventsProcessed = true";
        updateStackLimit();
        return true;
    default:
        qFatal("Unexpected jump result in event loop integration");
    }

    Q_UNREACHABLE();
}

+ (void)qt_eventDispatcherInterruptEventLoopExec
{
    // We re-set applicationProcessEventsReturnPoint here so that future
    // calls to QEventLoop::exec() will end up back here after entering
    // processEvents, instead of back in didFinishLaunchingWithOptions.
    switch (setjmp(processEventEnterJumpPoint)) {
    case kJumpPointSetSuccessfully:
        qCDebug(lcEventDispatcher) << "Jumping into processEvents due to system runloop exit ⇢";
        logActivity.UIApplicationMain.leave();
        longjmp(processEventExitJumpPoint, kJumpedFromEventLoopExecInterrupt);
        break;
    case kJumpedFromEventDispatcherProcessEvents:
        // QEventLoop was re-executed
        logActivity.UIApplicationMain.enter();
        qCDebug(lcEventDispatcher) << "↳ Jumped from processEvents due to re-exec";
        restoreStackLimit();
        break;
    default:
        qFatal("Unexpected jump result in event loop integration");
    }
}

+ (void)qt_applicationWillTerminate
{
    QAppleLogActivity applicationWillTerminateActivity = QT_APPLE_LOG_ACTIVITY_WITH_PARENT(
        lcEventDispatcher().isDebugEnabled(), "applicationWillTerminate", logActivity.UIApplicationMain).enter();
    qCDebug(lcEventDispatcher) << "Application about to be terminated by iOS";

    if (![self qt_rootLevelRunLoopIntegration])
        return;

    // Normally iOS just sends SIGKILL to quit applications, in which case there's
    // no chance for us to clean up anything, but in some rare cases iOS will tell
    // us that the application is about to be terminated.

    // We try to play nice with Qt by ending the main event loop, which will result
    // in QCoreApplication::aboutToQuit() being emitted, and main() returning to the
    // trampoline. The trampoline then redirects us back here, so that we can return
    // to UIApplicationMain instead of calling exit().

    applicationAboutToTerminate = true;
    switch (setjmp(applicationWillTerminateJumpPoint)) {
    case kJumpPointSetSuccessfully:
        qCDebug(lcEventDispatcher) << "Exiting qApp with SIGTERM exit code";
        qApp->exit([self qt_applicationWillTerminateExitCode]);

        // The runloop will not exit when the application is about to terminate,
        // so we'll never see the exit activity and have a chance to return from
        // QEventLoop::exec(). We initiate the return manually as a workaround.
        qCDebug(lcEventDispatcher) << "Manually triggering return from event loop exec";
        applicationWillTerminateActivity.leave();
        [self qt_eventDispatcherInterruptEventLoopExec];
        break;
    case kJumpedFromUserMainTrampoline:
        applicationWillTerminateActivity.enter();
        // The user's main has returned, so we're ready to let iOS terminate the application
        qCDebug(lcEventDispatcher) << "kJumpedFromUserMainTrampoline, allowing iOS to terminate";
        applicationWillTerminateActivity.leave();
        break;
    default:
        qFatal("Unexpected jump result in event loop integration");
    }
}

+ (bool)qt_applicationAboutToTerminate
{
    return applicationAboutToTerminate;
}

+ (char)qt_applicationWillTerminateExitCode
{
    // We treat applicationWillTerminate as SIGTERM, even if it can't be ignored,
    // and follow the bash convention of encoding the signal number in the upper
    // four bits of the exit code (exit(3) will only pass on the lower 8 bits).
    return char(SIGTERM | 0x80);
}

@end
