/* $Id: manifest.cpp $ */
/** @file
 * IPRT - Manifest file handling, old style - deprecated.
 */

/*
 * Copyright (C) 2009-2023 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
 */


/*********************************************************************************************************************************
*   Header Files                                                                                                                 *
*********************************************************************************************************************************/
#include "internal/iprt.h"
#include <iprt/manifest.h>

#include <iprt/err.h>
#include <iprt/file.h>
#include <iprt/mem.h>
#include <iprt/path.h>
#include <iprt/sha.h>
#include <iprt/stream.h>
#include <iprt/string.h>


/*********************************************************************************************************************************
*   Structures and Typedefs                                                                                                      *
*********************************************************************************************************************************/
/**
 * Internal per file structure used by RTManifestVerify
 */
typedef struct RTMANIFESTFILEENTRY
{
    char *pszManifestFile;
    char *pszManifestDigest;
    PRTMANIFESTTEST pTestPattern;
} RTMANIFESTFILEENTRY;
typedef RTMANIFESTFILEENTRY* PRTMANIFESTFILEENTRY;

/**
 * Internal structure used for the progress callback
 */
typedef struct RTMANIFESTCALLBACKDATA
{
    PFNRTPROGRESS pfnProgressCallback;
    void *pvUser;
    size_t cMaxFiles;
    size_t cCurrentFile;
} RTMANIFESTCALLBACKDATA;
typedef RTMANIFESTCALLBACKDATA* PRTMANIFESTCALLBACKDATA;


/*******************************************************************************
*   Private functions
*******************************************************************************/

DECLINLINE(char *) rtManifestPosOfCharInBuf(char const *pv, size_t cb, char c)
{
    char *pb = (char *)pv;
    for (; cb; --cb, ++pb)
        if (RT_UNLIKELY(*pb == c))
            return pb;
    return NULL;
}

DECLINLINE(size_t) rtManifestIndexOfCharInBuf(char const *pv, size_t cb, char c)
{
    char const *pb = (char const *)pv;
    for (size_t i=0; i < cb; ++i, ++pb)
        if (RT_UNLIKELY(*pb == c))
            return i;
    return cb;
}

static DECLCALLBACK(int) rtSHAProgressCallback(unsigned uPercent, void *pvUser)
{
    PRTMANIFESTCALLBACKDATA pData = (PRTMANIFESTCALLBACKDATA)pvUser;
    return pData->pfnProgressCallback((unsigned)(  (uPercent + (float)pData->cCurrentFile * 100.0)
                                                 / (float)pData->cMaxFiles),
                                      pData->pvUser);
}


/*******************************************************************************
*   Public functions
*******************************************************************************/

RTR3DECL(int) RTManifestVerify(const char *pszManifestFile, PRTMANIFESTTEST paTests, size_t cTests, size_t *piFailed)
{
    /* Validate input */
    AssertPtrReturn(pszManifestFile, VERR_INVALID_POINTER);

    /* Open the manifest file */
    RTFILE file;
    int rc = RTFileOpen(&file, pszManifestFile, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_NONE);
    if (RT_FAILURE(rc))
        return rc;

    void *pvBuf = 0;
    do
    {
        uint64_t cbSize;
        rc = RTFileQuerySize(file, &cbSize);
        if (RT_FAILURE(rc))
            break;

        /* Cast down for the case size_t < uint64_t. This isn't really correct,
           but we consider manifest files bigger than size_t as not supported
           by now. */
        size_t cbToRead = (size_t)cbSize;
        pvBuf = RTMemAlloc(cbToRead);
        if (!pvBuf)
        {
            rc = VERR_NO_MEMORY;
            break;
        }

        size_t cbRead = 0;
        rc = RTFileRead(file, pvBuf, cbToRead, &cbRead);
        if (RT_FAILURE(rc))
            break;

        rc = RTManifestVerifyFilesBuf(pvBuf, cbRead, paTests, cTests, piFailed);
    }while (0);

    /* Cleanup */
    if (pvBuf)
        RTMemFree(pvBuf);

    RTFileClose(file);

    return rc;
}

RTR3DECL(int) RTManifestVerifyFiles(const char *pszManifestFile, const char * const *papszFiles, size_t cFiles, size_t *piFailed,
                                    PFNRTPROGRESS pfnProgressCallback, void *pvUser)
{
    /* Validate input */
    AssertPtrReturn(pszManifestFile, VERR_INVALID_POINTER);
    AssertPtrReturn(papszFiles, VERR_INVALID_POINTER);
    AssertPtrNullReturn(pfnProgressCallback, VERR_INVALID_POINTER);

    int rc = VINF_SUCCESS;

    /* Create our compare list */
    PRTMANIFESTTEST paFiles = (PRTMANIFESTTEST)RTMemTmpAllocZ(sizeof(RTMANIFESTTEST) * cFiles);
    if (!paFiles)
        return VERR_NO_MEMORY;

    RTMANIFESTCALLBACKDATA callback = { pfnProgressCallback, pvUser, cFiles, 0 };
    /* Fill our compare list */
    for (size_t i = 0; i < cFiles; ++i)
    {
        char *pszDigest;
        if (pfnProgressCallback)
        {
            callback.cCurrentFile = i;
            rc = RTSha1DigestFromFile(papszFiles[i], &pszDigest, rtSHAProgressCallback, &callback);
        }
        else
            rc = RTSha1DigestFromFile(papszFiles[i], &pszDigest, NULL, NULL);
        if (RT_FAILURE(rc))
            break;
        paFiles[i].pszTestFile = (char*)papszFiles[i];
        paFiles[i].pszTestDigest = pszDigest;
    }

    /* Do the verification */
    if (RT_SUCCESS(rc))
        rc = RTManifestVerify(pszManifestFile, paFiles, cFiles, piFailed);

    /* Cleanup */
    for (size_t i = 0; i < cFiles; ++i)
    {
        if (paFiles[i].pszTestDigest)
            RTStrFree((char*)paFiles[i].pszTestDigest);
    }
    RTMemTmpFree(paFiles);

    return rc;
}

RTR3DECL(int) RTManifestWriteFiles(const char *pszManifestFile, RTDIGESTTYPE enmDigestType,
                                   const char * const *papszFiles, size_t cFiles,
                                   PFNRTPROGRESS pfnProgressCallback, void *pvUser)
{
    /* Validate input */
    AssertPtrReturn(pszManifestFile, VERR_INVALID_POINTER);
    AssertPtrReturn(papszFiles, VERR_INVALID_POINTER);
    AssertPtrNullReturn(pfnProgressCallback, VERR_INVALID_POINTER);

    RTFILE file;
    int rc = RTFileOpen(&file, pszManifestFile, RTFILE_O_CREATE | RTFILE_O_WRITE | RTFILE_O_DENY_ALL);
    if (RT_FAILURE(rc))
        return rc;

    PRTMANIFESTTEST paFiles = 0;
    void *pvBuf = 0;
    do
    {
        paFiles = (PRTMANIFESTTEST)RTMemAllocZ(sizeof(RTMANIFESTTEST) * cFiles);
        if (!paFiles)
        {
            rc = VERR_NO_MEMORY;
            break;
        }

        RTMANIFESTCALLBACKDATA callback = { pfnProgressCallback, pvUser, cFiles, 0 };
        for (size_t i = 0; i < cFiles; ++i)
        {
            paFiles[i].pszTestFile = papszFiles[i];
            /* Calculate the SHA1 digest of every file */
            if (pfnProgressCallback)
            {
                callback.cCurrentFile = i;
                rc = RTSha1DigestFromFile(paFiles[i].pszTestFile, (char**)&paFiles[i].pszTestDigest, rtSHAProgressCallback, &callback);
            }
            else
                rc = RTSha1DigestFromFile(paFiles[i].pszTestFile, (char**)&paFiles[i].pszTestDigest, NULL, NULL);
            if (RT_FAILURE(rc))
                break;
        }

        if (RT_SUCCESS(rc))
        {
            size_t cbSize = 0;
            rc = RTManifestWriteFilesBuf(&pvBuf, &cbSize, enmDigestType, paFiles, cFiles);
            if (RT_FAILURE(rc))
                break;

            rc = RTFileWrite(file, pvBuf, cbSize, 0);
        }
    }while (0);

    RTFileClose(file);

    /* Cleanup */
    if (pvBuf)
        RTMemFree(pvBuf);
    if (paFiles)
    {
        for (size_t i = 0; i < cFiles; ++i)
            if (paFiles[i].pszTestDigest)
                RTStrFree((char*)paFiles[i].pszTestDigest);
        RTMemFree(paFiles);
    }

    /* Delete the manifest file on failure */
    if (RT_FAILURE(rc))
        RTFileDelete(pszManifestFile);

    return rc;
}


RTR3DECL(int) RTManifestVerifyDigestType(void const *pvBuf, size_t cbSize, RTDIGESTTYPE *penmDigestType)
{
    /* Validate input */
    AssertPtrReturn(pvBuf, VERR_INVALID_POINTER);
    AssertReturn(cbSize > 0, VERR_INVALID_PARAMETER);
    AssertPtrReturn(penmDigestType, VERR_INVALID_POINTER);

    int rc = VINF_SUCCESS;

    char const *pcBuf = (char *)pvBuf;
    size_t cbRead = 0;
    /* Parse the manifest file line by line */
    for (;;)
    {
        if (cbRead >= cbSize)
            return VERR_MANIFEST_UNSUPPORTED_DIGEST_TYPE;

        size_t cch = rtManifestIndexOfCharInBuf(pcBuf, cbSize - cbRead, '\n') + 1;

        /* Skip empty lines (UNIX/DOS format) */
        if (   (   cch == 1
                && pcBuf[0] == '\n')
            || (   cch == 2
                && pcBuf[0] == '\r'
                && pcBuf[1] == '\n'))
        {
            pcBuf += cch;
            cbRead += cch;
            continue;
        }

/** @todo r=bird: Missing space check here. */
        /* Check for the digest algorithm */
        if (   pcBuf[0] == 'S'
            && pcBuf[1] == 'H'
            && pcBuf[2] == 'A'
            && pcBuf[3] == '1')
        {
            *penmDigestType = RTDIGESTTYPE_SHA1;
            break;
        }
        if (   pcBuf[0] == 'S'
            && pcBuf[1] == 'H'
            && pcBuf[2] == 'A'
            && pcBuf[3] == '2'
            && pcBuf[4] == '5'
            && pcBuf[5] == '6')
        {
            *penmDigestType = RTDIGESTTYPE_SHA256;
            break;
        }

        pcBuf += cch;
        cbRead += cch;
    }

    return rc;
}


RTR3DECL(int) RTManifestVerifyFilesBuf(void *pvBuf, size_t cbSize, PRTMANIFESTTEST paTests, size_t cTests, size_t *piFailed)
{
    /* Validate input */
    AssertPtrReturn(pvBuf, VERR_INVALID_POINTER);
    AssertReturn(cbSize > 0, VERR_INVALID_PARAMETER);
    AssertPtrReturn(paTests, VERR_INVALID_POINTER);
    AssertReturn(cTests > 0, VERR_INVALID_PARAMETER);
    AssertPtrNullReturn(piFailed, VERR_INVALID_POINTER);

    int rc = VINF_SUCCESS;

    PRTMANIFESTFILEENTRY paFiles = (PRTMANIFESTFILEENTRY)RTMemTmpAllocZ(sizeof(RTMANIFESTFILEENTRY) * cTests);
    if (!paFiles)
        return VERR_NO_MEMORY;

    /* Fill our compare list */
    for (size_t i = 0; i < cTests; ++i)
        paFiles[i].pTestPattern = &paTests[i];

    char *pcBuf = (char*)pvBuf;
    size_t cbRead = 0;
    /* Parse the manifest file line by line */
    for (;;)
    {
        if (cbRead >= cbSize)
            break;

        size_t cch = rtManifestIndexOfCharInBuf(pcBuf, cbSize - cbRead, '\n') + 1;

        /* Skip empty lines (UNIX/DOS format) */
        if (   (   cch == 1
                && pcBuf[0] == '\n')
            || (   cch == 2
                && pcBuf[0] == '\r'
                && pcBuf[1] == '\n'))
        {
            pcBuf += cch;
            cbRead += cch;
            continue;
        }

        /** @todo r=bird:
         *  -# Better deal with this EOF line platform dependency
         *  -# The SHA1 and SHA256 tests should probably include a blank space check.
         *  -# If there is a specific order to the elements in the string, it would be
         *     good if the delimiter searching checked for it.
         *  -# Deal with filenames containing delimiter characters.
         */

        /* Check for the digest algorithm */
        if (   cch < 4
            || (   !(   pcBuf[0] == 'S'
                     && pcBuf[1] == 'H'
                     && pcBuf[2] == 'A'
                     && pcBuf[3] == '1')
                &&
                   !(   pcBuf[0] == 'S'
                     && pcBuf[1] == 'H'
                     && pcBuf[2] == 'A'
                     && pcBuf[3] == '2'
                     && pcBuf[4] == '5'
                     && pcBuf[5] == '6')
               )
            )
        {
            /* Digest unsupported */
            rc = VERR_MANIFEST_UNSUPPORTED_DIGEST_TYPE;
            break;
        }

        /* Try to find the filename */
        char *pszNameStart = rtManifestPosOfCharInBuf(pcBuf, cch, '(');
        if (!pszNameStart)
        {
            rc = VERR_MANIFEST_WRONG_FILE_FORMAT;
            break;
        }
        char *pszNameEnd = rtManifestPosOfCharInBuf(pcBuf, cch, ')');
        if (!pszNameEnd)
        {
            rc = VERR_MANIFEST_WRONG_FILE_FORMAT;
            break;
        }

        /* Copy the filename part */
        size_t cchName = pszNameEnd - pszNameStart - 1;
        char *pszName = (char *)RTMemTmpAlloc(cchName + 1);
        if (!pszName)
        {
            rc = VERR_NO_MEMORY;
            break;
        }
        memcpy(pszName, pszNameStart + 1, cchName);
        pszName[cchName] = '\0';

        /* Try to find the digest sum */
        char *pszDigestStart = rtManifestPosOfCharInBuf(pcBuf, cch, '=') + 1;
        if (!pszDigestStart)
        {
            RTMemTmpFree(pszName);
            rc = VERR_MANIFEST_WRONG_FILE_FORMAT;
            break;
        }
        char *pszDigestEnd = rtManifestPosOfCharInBuf(pcBuf, cch, '\r');
        if (!pszDigestEnd)
            pszDigestEnd = rtManifestPosOfCharInBuf(pcBuf, cch, '\n');
        if (!pszDigestEnd)
        {
            RTMemTmpFree(pszName);
            rc = VERR_MANIFEST_WRONG_FILE_FORMAT;
            break;
        }
        /* Copy the digest part */
        size_t cchDigest = pszDigestEnd - pszDigestStart - 1;
        char *pszDigest = (char *)RTMemTmpAlloc(cchDigest + 1);
        if (!pszDigest)
        {
            RTMemTmpFree(pszName);
            rc = VERR_NO_MEMORY;
            break;
        }
        memcpy(pszDigest, pszDigestStart + 1, cchDigest);
        pszDigest[cchDigest] = '\0';

        /* Check our file list against the extracted data */
        bool fFound = false;
        for (size_t i = 0; i < cTests; ++i)
        {
            /** @todo r=bird: Using RTStrStr here looks bogus. */
            if (RTStrStr(paFiles[i].pTestPattern->pszTestFile, RTStrStrip(pszName)) != NULL)
            {
                /* Add the data of the manifest file to the file list */
                paFiles[i].pszManifestFile = RTStrDup(RTStrStrip(pszName));
                paFiles[i].pszManifestDigest = RTStrDup(RTStrStrip(pszDigest));
                fFound = true;
                break;
            }
        }
        RTMemTmpFree(pszName);
        RTMemTmpFree(pszDigest);
        if (!fFound)
        {
            /* There have to be an entry in the file list */
            rc = VERR_MANIFEST_FILE_MISMATCH;
            break;
        }

        pcBuf += cch;
        cbRead += cch;
    }

    if (   rc == VINF_SUCCESS
        || rc == VERR_EOF)
    {
        rc = VINF_SUCCESS;
        for (size_t i = 0; i < cTests; ++i)
        {
            /* If there is an entry in the file list, which hasn't an
             * equivalent in the manifest file, its an error. */
            if (   !paFiles[i].pszManifestFile
                || !paFiles[i].pszManifestDigest)
            {
                rc = VERR_MANIFEST_FILE_MISMATCH;
                break;
            }

            /* Do the manifest SHA digest match against the actual digest? */
            if (RTStrICmp(paFiles[i].pszManifestDigest, paFiles[i].pTestPattern->pszTestDigest))
            {
                if (piFailed)
                    *piFailed = i;
                rc = VERR_MANIFEST_DIGEST_MISMATCH;
                break;
            }
        }
    }

    /* Cleanup */
    for (size_t i = 0; i < cTests; ++i)
    {
        if (paFiles[i].pszManifestFile)
            RTStrFree(paFiles[i].pszManifestFile);
        if (paFiles[i].pszManifestDigest)
            RTStrFree(paFiles[i].pszManifestDigest);
    }
    RTMemTmpFree(paFiles);

    return rc;
}

RTR3DECL(int) RTManifestWriteFilesBuf(void **ppvBuf, size_t *pcbSize, RTDIGESTTYPE enmDigestType, PRTMANIFESTTEST paFiles, size_t cFiles)
{
    /* Validate input */
    AssertPtrReturn(ppvBuf, VERR_INVALID_POINTER);
    AssertPtrReturn(pcbSize, VERR_INVALID_POINTER);
    AssertPtrReturn(paFiles, VERR_INVALID_POINTER);
    AssertReturn(cFiles > 0, VERR_INVALID_PARAMETER);

    const char *pcszDigestType;
    switch (enmDigestType)
    {
        case RTDIGESTTYPE_CRC32:  pcszDigestType = "CRC32";  break;
        case RTDIGESTTYPE_CRC64:  pcszDigestType = "CRC64";  break;
        case RTDIGESTTYPE_MD5:    pcszDigestType = "MD5";    break;
        case RTDIGESTTYPE_SHA1:   pcszDigestType = "SHA1";   break;
        case RTDIGESTTYPE_SHA256: pcszDigestType = "SHA256"; break;
        default: return VERR_INVALID_PARAMETER;
    }

    /* Calculate the size necessary for the memory buffer. */
    size_t cbSize = 0;
    size_t cbMaxSize = 0;
    for (size_t i = 0; i < cFiles; ++i)
    {
        size_t cbTmp = strlen(RTPathFilename(paFiles[i].pszTestFile))
                     + strlen(paFiles[i].pszTestDigest)
                     + strlen(pcszDigestType)
                     + 6;
        cbMaxSize = RT_MAX(cbMaxSize, cbTmp);
        cbSize += cbTmp;
    }

    /* Create the memory buffer */
    void *pvBuf = RTMemAlloc(cbSize);
    if (!pvBuf)
        return VERR_NO_MEMORY;

    /* Allocate a temporary string buffer. */
    char *pszTmp = RTStrAlloc(cbMaxSize + 1);
    if (!pszTmp)
    {
        RTMemFree(pvBuf);
        return VERR_NO_MEMORY;
    }
    size_t cbPos = 0;

    for (size_t i = 0; i < cFiles; ++i)
    {
        size_t cch = RTStrPrintf(pszTmp, cbMaxSize + 1, "%s (%s)= %s\n", pcszDigestType, RTPathFilename(paFiles[i].pszTestFile), paFiles[i].pszTestDigest);
        memcpy(&((char*)pvBuf)[cbPos], pszTmp, cch);
        cbPos += cch;
    }
    RTStrFree(pszTmp);

    /* Results */
    *ppvBuf = pvBuf;
    *pcbSize = cbSize;

    return VINF_SUCCESS;
}

