/* $Id: strformattype.cpp $ */
/** @file
 * IPRT - IPRT String Formatter Extensions, Dynamic Types.
 */

/*
 * Copyright (C) 2008-2025 Oracle and/or its affiliates.
 *
 * This file is part of VirtualBox base platform packages, as
 * available from https://www.virtualbox.org.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, in version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses>.
 *
 * The contents of this file may alternatively be used under the terms
 * of the Common Development and Distribution License Version 1.0
 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
 * in the VirtualBox distribution, in which case the provisions of the
 * CDDL are applicable instead of those of the GPL.
 *
 * You may elect to license modified versions of this file under the
 * terms and conditions of either the GPL or the CDDL or both.
 *
 * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
 */


/*********************************************************************************************************************************
*   Global Variables                                                                                                             *
*********************************************************************************************************************************/
#define LOG_GROUP RTLOGGROUP_STRING
#include <iprt/string.h>
#include "internal/iprt.h"

#include <iprt/asm.h>
#include <iprt/assert.h>
#include <iprt/err.h>
#include <iprt/stdarg.h>
#include "internal/string.h"


/*********************************************************************************************************************************
*   Defined Constants And Macros                                                                                                 *
*********************************************************************************************************************************/
#ifdef RT_STRICT
# define RTSTRFORMATTYPE_WITH_LOCKING
#endif
#ifdef RTSTRFORMATTYPE_WITH_LOCKING
# define RTSTRFORMATTYPE_LOCK_OFFSET    0x7fff0000
#endif


/*********************************************************************************************************************************
*   Structures and Typedefs                                                                                                      *
*********************************************************************************************************************************/
/**
 * Description of a registered formatting type.
 *
 * In GC we'll be using offsets instead of pointers just to try avoid having to
 * do the bothersome relocating. This of course assumes that all the relevant
 * code stays within the same mapping.
 */
typedef struct RTSTRDYNFMT
{
    /** The length of the type. */
    uint8_t             cchType;
    /** The type name. */
    char                szType[47];
    /** The handler function.
     * In GC the offset is relative to g_aTypes[0], so that &g_aTypes[0] + offHandler
     * gives the actual address. */
#ifdef IN_RC
    int32_t             offHandler;
#else
    PFNRTSTRFORMATTYPE  pfnHandler;
#endif
    /** Callback argument. */
    void * volatile     pvUser;
#if ARCH_BITS == 32
    /** Size alignment padding. */
    char                abPadding[8];
#endif
} RTSTRDYNFMT;
AssertCompileSizeAlignment(RTSTRDYNFMT, 32);
typedef RTSTRDYNFMT *PRTSTRDYNFMT;
typedef RTSTRDYNFMT const *PCRTSTRDYNFMT;


/*********************************************************************************************************************************
*   Global Variables                                                                                                             *
*********************************************************************************************************************************/
/** The registered types, sorted for binary lookup.
 * We use a static array here because it avoids RTMemAlloc dependencies+leaks. */
static RTSTRDYNFMT      g_aTypes[64];
/** The number of registered types. */
static uint32_t         g_cTypes = 0;
#ifdef RTSTRFORMATTYPE_WITH_LOCKING
/** This is just a thing we assert/spin on.
 * Zero == unlocked, negative == write locked, positive == read locked.
 *
 * The user should do all the serialization and we'll smack his fingers in
 * strict builds if he doesn't. */
static int32_t volatile g_i32Spinlock = 0;
#endif


/**
 * Locks the stuff for updating.
 *
 * Mostly for check that the caller is doing his job.
 */
DECLINLINE(void) rtstrFormatTypeWriteLock(void)
{
#if defined(RTSTRFORMATTYPE_WITH_LOCKING)
    if (RT_UNLIKELY(!ASMAtomicCmpXchgS32(&g_i32Spinlock, -RTSTRFORMATTYPE_LOCK_OFFSET, 0)))
    {
        unsigned volatile i;

        AssertFailed();
        for (i = 0;; i++)
            if (    !g_i32Spinlock
                &&  ASMAtomicCmpXchgS32(&g_i32Spinlock, -RTSTRFORMATTYPE_LOCK_OFFSET, 0))
                break;
    }
#endif
}


/**
 * Undoing rtstrFormatTypeWriteLock.
 */
DECLINLINE(void) rtstrFormatTypeWriteUnlock(void)
{
#if defined(RTSTRFORMATTYPE_WITH_LOCKING)
    Assert(g_i32Spinlock < 0);
    ASMAtomicAddS32(&g_i32Spinlock, RTSTRFORMATTYPE_LOCK_OFFSET);
#endif
}


/**
 * Locks the stuff for reading.
 *
 * This is just cheap stuff to make sure the caller is doing the right thing.
 */
DECLINLINE(void) rtstrFormatTypeReadLock(void)
{
#if defined(RTSTRFORMATTYPE_WITH_LOCKING)
    if (RT_UNLIKELY(ASMAtomicIncS32(&g_i32Spinlock) < 0))
    {
        unsigned volatile i;

        AssertFailed();
        for (i = 0;; i++)
            if (ASMAtomicUoReadS32(&g_i32Spinlock) > 0)
                break;
    }
#endif
}


/**
 * Undoing rtstrFormatTypeReadLock.
 */
DECLINLINE(void) rtstrFormatTypeReadUnlock(void)
{
#if defined(RTSTRFORMATTYPE_WITH_LOCKING)
    Assert(g_i32Spinlock > 0);
    ASMAtomicDecS32(&g_i32Spinlock);
#endif
}


/**
 * Compares a type string with a type entry, the string doesn't need to be terminated.
 *
 * @returns Same as memcmp.
 * @param   pszType     The type string, doesn't need to be terminated.
 * @param   cchType     The number of chars in @a pszType to compare.
 * @param   pType       The type entry to compare with.
 */
DECLINLINE(int) rtstrFormatTypeCompare(const char *pszType, size_t cchType, PCRTSTRDYNFMT pType)
{
    size_t cch = RT_MIN(cchType, pType->cchType);
    int iDiff = memcmp(pszType, pType->szType, cch);
    if (!iDiff)
    {
        if (cchType == pType->cchType)
            return 0;
        iDiff = cchType < pType->cchType ? -1 : 1;
    }
    return iDiff;
}


/**
 * Looks up a type entry.
 *
 * @returns The type index, -1 on failure.
 * @param   pszType     The type to look up. This doesn't have to be terminated.
 * @param   cchType     The length of the type.
 */
DECLINLINE(int32_t) rtstrFormatTypeLookup(const char *pszType, size_t cchType)
{
    /*
     * Lookup the type - binary search.
     */
    int32_t iStart = 0;
    int32_t iEnd   = g_cTypes - 1;
    int32_t i      = iEnd / 2;
    for (;;)
    {
        int iDiff = rtstrFormatTypeCompare(pszType, cchType, &g_aTypes[i]);
        if (!iDiff)
            return i;
        if (iEnd == iStart)
            break;
        if (iDiff < 0)
            iEnd = i - 1;
        else
            iStart = i + 1;
        if (iEnd < iStart)
            break;
        i = iStart + (iEnd - iStart) / 2;
    }
    return -1;
}


/**
 * Register a format handler for a type.
 *
 * The format handler is used to handle '%R[type]' format types, where the argument
 * in the vector is a pointer value (a bit restrictive, but keeps it simple).
 *
 * The caller must ensure that no other thread will be making use of any of
 * the dynamic formatting type facilities simultaneously with this call.
 *
 * @returns IPRT status code.
 * @retval  VINF_SUCCESS on success.
 * @retval  VERR_ALREADY_EXISTS if the type has already been registered.
 * @retval  VERR_TOO_MANY_OPEN_FILES if all the type slots has been allocated already.
 *
 * @param   pszType         The type name.
 * @param   pfnHandler      The handler address. See FNRTSTRFORMATTYPE for details.
 * @param   pvUser          The user argument to pass to the handler. See RTStrFormatTypeSetUser
 *                          for how to update this later.
 */
RTDECL(int) RTStrFormatTypeRegister(const char *pszType, PFNRTSTRFORMATTYPE pfnHandler, void *pvUser)
{
    int rc;
    size_t cchType;
    uint32_t cTypes;

    /*
     * Validate input.
     */
    AssertPtr(pfnHandler);
    AssertPtr(pszType);
    cchType = strlen(pszType);
    AssertReturn(cchType < RT_SIZEOFMEMB(RTSTRDYNFMT, szType), VERR_INVALID_PARAMETER);

    /*
     * Try add it.
     */
    rtstrFormatTypeWriteLock();

    /* check that there are empty slots. */
    cTypes = g_cTypes;
    if (cTypes < RT_ELEMENTS(g_aTypes))
    {
        /* find where to insert it. */
        uint32_t i = 0;
        rc = VINF_SUCCESS;
        while (i < cTypes)
        {
            int iDiff = rtstrFormatTypeCompare(pszType, cchType, &g_aTypes[i]);
            if (!iDiff)
            {
                rc = VERR_ALREADY_EXISTS;
                break;
            }
            if (iDiff < 0)
                break;
            i++;
        }
        if (RT_SUCCESS(rc))
        {
            /* make room. */
            uint32_t cToMove = cTypes - i;
            if (cToMove)
                memmove(&g_aTypes[i + 1], &g_aTypes[i], cToMove * sizeof(g_aTypes[i]));

            /* insert the new entry. */
            memset(&g_aTypes[i], 0, sizeof(g_aTypes[i]));
            memcpy(&g_aTypes[i].szType[0], pszType, cchType + 1);
            g_aTypes[i].cchType = (uint8_t)cchType;
            g_aTypes[i].pvUser = pvUser;
#ifdef IN_RC
            g_aTypes[i].offHandler = (intptr_t)pfnHandler - (intptr_t)&g_aTypes[0];
#else
            g_aTypes[i].pfnHandler = pfnHandler;
#endif
            ASMAtomicIncU32(&g_cTypes);
            rc = VINF_SUCCESS;
        }
    }
    else
        rc = VERR_TOO_MANY_OPEN_FILES; /** @todo fix error code */

    rtstrFormatTypeWriteUnlock();

    return rc;
}
RT_EXPORT_SYMBOL(RTStrFormatTypeRegister);


/**
 * Deregisters a format type.
 *
 * The caller must ensure that no other thread will be making use of any of
 * the dynamic formatting type facilities simultaneously with this call.
 *
 * @returns IPRT status code.
 * @retval  VINF_SUCCESS on success.
 * @retval  VERR_FILE_NOT_FOUND if not found.
 *
 * @param   pszType     The type to deregister.
 */
RTDECL(int) RTStrFormatTypeDeregister(const char *pszType)
{
    int32_t i;

    /*
     * Validate input.
     */
    AssertPtr(pszType);

    /*
     * Locate the entry and remove it.
     */
    rtstrFormatTypeWriteLock();
    i = rtstrFormatTypeLookup(pszType, strlen(pszType));
    if (i >= 0)
    {
        const uint32_t cTypes = g_cTypes;
        int32_t cToMove = cTypes - i - 1;
        if (cToMove > 0)
            memmove(&g_aTypes[i], &g_aTypes[i + 1], cToMove * sizeof(g_aTypes[i]));
        memset(&g_aTypes[cTypes - 1], 0, sizeof(g_aTypes[0]));
        ASMAtomicDecU32(&g_cTypes);
    }
    rtstrFormatTypeWriteUnlock();

    Assert(i >= 0);
    return i >= 0
         ? VINF_SUCCESS
         : VERR_FILE_NOT_FOUND; /** @todo fix status code */
}
RT_EXPORT_SYMBOL(RTStrFormatTypeDeregister);


/**
 * Sets the user argument for a type.
 *
 * This can be used if a user argument needs relocating in GC.
 *
 * @returns IPRT status code.
 * @retval  VINF_SUCCESS on success.
 * @retval  VERR_FILE_NOT_FOUND if not found.
 *
 * @param   pszType     The type to update.
 * @param   pvUser      The new user argument value.
 */
RTDECL(int) RTStrFormatTypeSetUser(const char *pszType, void *pvUser)
{
    int32_t i;

    /*
     * Validate input.
     */
    AssertPtr(pszType);

    /*
     * Locate the entry and update it.
     */
    rtstrFormatTypeReadLock();

    i = rtstrFormatTypeLookup(pszType, strlen(pszType));
    if (i >= 0)
        ASMAtomicWritePtr(&g_aTypes[i].pvUser, pvUser);

    rtstrFormatTypeReadUnlock();

    Assert(i >= 0);
    return i >= 0
         ? VINF_SUCCESS
         : VERR_FILE_NOT_FOUND; /** @todo fix status code */
}
RT_EXPORT_SYMBOL(RTStrFormatTypeSetUser);


/**
 * Formats a type using a registered callback handler.
 *
 * This will handle %R[type].
 *
 * @returns The number of bytes formatted.
 * @param   pfnOutput       Pointer to output function.
 * @param   pvArgOutput     Argument for the output function.
 * @param   ppszFormat      Pointer to the format string pointer. Advance this till the char
 *                          after the format specifier.
 * @param   pArgs           Pointer to the argument list. Use this to fetch the arguments.
 * @param   cchWidth        Format Width. -1 if not specified.
 * @param   cchPrecision    Format Precision. -1 if not specified.
 * @param   fFlags          Flags (RTSTR_NTFS_*).
 * @param   chArgSize       The argument size specifier, 'l' or 'L'.
 */
DECLHIDDEN(size_t) rtstrFormatType(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput, const char **ppszFormat,
                                   va_list *pArgs, int cchWidth, int cchPrecision, unsigned fFlags, char chArgSize)
{
    size_t      cch;
    int32_t     i;
    char const *pszTypeEnd;
    char const *pszType;
    char        ch;
    void       *pvValue = va_arg(*pArgs, void *);
    NOREF(chArgSize);

    /*
     * Parse out the type.
     */
    pszType = *ppszFormat + 2;
    *ppszFormat = pszType;
    Assert(pszType[-1] == '[');
    Assert(pszType[-2] == 'R');
    pszTypeEnd = pszType;
    while ((ch = *pszTypeEnd) != ']')
    {
        AssertReturn(ch != '\0', 0);
        AssertReturn(ch != '%', 0);
        AssertReturn(ch != '[', 0);
        pszTypeEnd++;
    }
    *ppszFormat = pszTypeEnd + 1;

    /*
     * Locate the entry and call the handler.
     */
    rtstrFormatTypeReadLock();

    i = rtstrFormatTypeLookup(pszType, pszTypeEnd - pszType);
    if (RT_LIKELY(i >= 0))
    {
#ifdef IN_RC
        PFNRTSTRFORMATTYPE pfnHandler = (PFNRTSTRFORMATTYPE)((intptr_t)&g_aTypes[0] + g_aTypes[i].offHandler);
#else
        PFNRTSTRFORMATTYPE pfnHandler = g_aTypes[i].pfnHandler;
#endif
        void *pvUser = ASMAtomicReadPtr(&g_aTypes[i].pvUser);

        rtstrFormatTypeReadUnlock();

        cch = pfnHandler(pfnOutput, pvArgOutput, g_aTypes[i].szType, pvValue, cchWidth, cchPrecision, fFlags, pvUser);
    }
    else
    {
        rtstrFormatTypeReadUnlock();

        cch  = pfnOutput(pvArgOutput, RT_STR_TUPLE("<missing:%R["));
        cch += pfnOutput(pvArgOutput, pszType, pszTypeEnd - pszType);
        cch += pfnOutput(pvArgOutput, RT_STR_TUPLE("]>"));
    }

    return cch;
}

