/* $Id: kDep.c 3315 2020-03-31 01:12:19Z bird $ */
/** @file
 * kDep - Common Dependency Managemnt Code.
 */

/*
 * Copyright (c) 2004-2013 knut st. osmundsen <bird-kBuild-spamx@anduin.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * Alternatively, the content of this file may be used under the terms of the
 * GPL version 2 or later, or LGPL version 2.1 or later.
 */


/*******************************************************************************
*   Header Files                                                               *
*******************************************************************************/
#ifdef KMK /* For when it gets compiled and linked into kmk. */
# include "makeint.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <limits.h>
#include <sys/stat.h>
#include "k/kDefs.h"
#include "k/kTypes.h"
#if K_OS == K_OS_WINDOWS
# define USE_WIN_MMAP
# include <io.h>
# include <Windows.h>
# include "nt_fullpath.h"
# include "nt/ntstat.h"
#else
# include <dirent.h>
# include <unistd.h>
# include <stdint.h>
#endif

#include "kDep.h"

#ifdef KWORKER
extern int kwFsPathExists(const char *pszPath);
#endif


/*********************************************************************************************************************************
*   Defined Constants And Macros                                                                                                 *
*********************************************************************************************************************************/
/* For the GNU/hurd weirdo. */
#if !defined(PATH_MAX) && !defined(_MAX_PATH)
# define PATH_MAX 4096
#endif


/**
 * Initializes the dep instance.
 *
 * @param   pThis       The dep instance to init.
 */
void depInit(PDEPGLOBALS pThis)
{
    pThis->pDeps = NULL;
}


/**
 * Cleans up the dep instance (frees resources).
 *
 * @param   pThis       The dep instance to cleanup.
 */
void depCleanup(PDEPGLOBALS pThis)
{
    PDEP pDep = pThis->pDeps;
    pThis->pDeps = NULL;
    while (pDep)
    {
        PDEP pFree = pDep;
        pDep = pDep->pNext;
        free(pFree);
    }
}


/**
 * Corrects all slashes to unix slashes.
 *
 * @returns pszFilename.
 * @param   pszFilename     The filename to correct.
 */
static char *fixslash(char *pszFilename)
{
    char *psz = pszFilename;
    while ((psz = strchr(psz, '\\')) != NULL)
        *psz++ = '/';
    return pszFilename;
}


#if K_OS == K_OS_OS2

/**
 * Corrects the case of a path.
 *
 * @param   pszPath     Pointer to the path, both input and output.
 *                      The buffer must be able to hold one more byte than the string length.
 */
static void fixcase(char *pszFilename)
{
    return;
}

#elif K_OS != K_OS_WINDOWS

/**
 * Corrects the case of a path.
 *
 * @param   pszPath     Pointer to the path, both input and output.
 */
static void fixcase(char *pszFilename)
{
    char *psz;

    /*
     * Skip the root.
     */
    psz = pszFilename;
    while (*psz == '/')
        psz++;

    /*
     * Iterate all the components.
     */
    while (*psz)
    {
        char  chSlash;
        struct stat s;
        char   *pszStart = psz;

        /*
         * Find the next slash (or end of string) and terminate the string there.
         */
        while (*psz != '/' && *psz)
            psz++;
        chSlash = *psz;
        *psz = '\0';

        /*
         * Does this part exist?
         * If not we'll enumerate the directory and search for an case-insensitive match.
         */
        if (stat(pszFilename, &s))
        {
            struct dirent  *pEntry;
            DIR            *pDir;
            if (pszStart == pszFilename)
                pDir = opendir(*pszFilename ? pszFilename : ".");
            else
            {
                pszStart[-1] = '\0';
                pDir = opendir(pszFilename);
                pszStart[-1] = '/';
            }
            if (!pDir)
            {
                *psz = chSlash;
                break; /* giving up, if we fail to open the directory. */
            }

            while ((pEntry = readdir(pDir)) != NULL)
            {
                if (!strcasecmp(pEntry->d_name, pszStart))
                {
                    strcpy(pszStart, pEntry->d_name);
                    break;
                }
            }
            closedir(pDir);
            if (!pEntry)
            {
                *psz = chSlash;
                break;  /* giving up if not found. */
            }
        }

        /* restore the slash and press on. */
        *psz = chSlash;
        while (*psz == '/')
            psz++;
    }

    return;
}

#endif /* !OS/2 && !Windows */


/**
 * 'Optimizes' and corrects the dependencies.
 */
void depOptimize(PDEPGLOBALS pThis, int fFixCase, int fQuiet, const char *pszIgnoredExt)
{
    /*
     * Walk the list correct the names and re-insert them.
     */
    size_t  cchIgnoredExt = pszIgnoredExt ? strlen(pszIgnoredExt) : 0;
    PDEP    pDepOrg = pThis->pDeps;
    PDEP    pDep = pThis->pDeps;
    pThis->pDeps = NULL;
    for (; pDep; pDep = pDep->pNext)
    {
#ifndef PATH_MAX
        char        szFilename[_MAX_PATH + 1];
#else
        char        szFilename[PATH_MAX + 1];
#endif
        char       *pszFilename;
#if !defined(KWORKER) && !defined(KMK)
        struct stat s;
#endif

        /*
         * Skip some fictive names like <built-in> and <command line>.
         */
        if (    pDep->szFilename[0] == '<'
            &&  pDep->szFilename[pDep->cchFilename - 1] == '>')
            continue;
        pszFilename = pDep->szFilename;

        /*
         * Skip pszIgnoredExt if given.
         */
        if (   pszIgnoredExt
            && pDep->cchFilename > cchIgnoredExt
            && memcmp(&pDep->szFilename[pDep->cchFilename - cchIgnoredExt], pszIgnoredExt, cchIgnoredExt) == 0)
            continue;

#if K_OS != K_OS_OS2 && K_OS != K_OS_WINDOWS
        /*
         * Skip any drive letters from compilers running in wine.
         */
        if (pszFilename[1] == ':')
            pszFilename += 2;
#endif

        /*
         * The microsoft compilers are notoriously screwing up the casing.
         * This will screw up kmk (/ GNU Make).
         */
        if (fFixCase)
        {
#if K_OS == K_OS_WINDOWS
            nt_fullpath_cached(pszFilename, szFilename, sizeof(szFilename));
            fixslash(szFilename);
#else
            strcpy(szFilename, pszFilename);
            fixslash(szFilename);
            fixcase(szFilename);
#endif
            pszFilename = szFilename;
        }

        /*
         * Check that the file exists before we start depending on it.
         */
        errno = 0;
#ifdef KWORKER
        if (!kwFsPathExists(pszFilename))
#elif defined(KMK)
        if (!file_exists_p(pszFilename))
#elif K_OS == K_OS_WINDOWS
        if (birdStatModTimeOnly(pszFilename, &s.st_mtim, 1 /*fFollowLink*/) != 0)
#else
        if (stat(pszFilename, &s) != 0)
#endif
        {
            if (   !fQuiet
                || errno != ENOENT
                || (   pszFilename[0] != '/'
                    && pszFilename[0] != '\\'
                    && (   !isalpha(pszFilename[0])
                        || pszFilename[1] != ':'
                        || (    pszFilename[2] != '/'
                            &&  pszFilename[2] != '\\')))
               )
                fprintf(stderr, "kDep: Skipping '%s' - %s!\n", pszFilename, strerror(errno));
            continue;
        }

        /*
         * Insert the corrected dependency.
         */
        depAdd(pThis, pszFilename, strlen(pszFilename));
    }

    /*
     * Free the old ones.
     */
    while (pDepOrg)
    {
        pDep = pDepOrg;
        pDepOrg = pDepOrg->pNext;
        free(pDep);
    }
}


/**
 * Write a filename that contains characters that needs escaping.
 *
 * @param   pOutput The output stream.
 * @param   pszFile The filename.
 * @param   cchFile The length of the filename.
 * @param   fDep    Whether this is for a dependency file or a target file.
 */
int depNeedsEscaping(const char *pszFile, size_t cchFile, int fDependency)
{
    return memchr(pszFile, ' ',  cchFile) != NULL
        || memchr(pszFile, '\t', cchFile) != NULL
        || memchr(pszFile, '#',  cchFile) != NULL
        || memchr(pszFile, '=',  cchFile) != NULL
        || memchr(pszFile, ';',  cchFile) != NULL
        || memchr(pszFile, '$',  cchFile) != NULL
        || memchr(pszFile, fDependency ? '|' : '%',  cchFile) != NULL;
}


/**
 * Write a filename that contains characters that needs escaping.
 *
 * @param   pOutput The output stream.
 * @param   pszFile The filename.
 * @param   cchFile The length of the filename.
 * @param   fDep    Whether this is for a dependency file or a target file.
 */
void depEscapedWrite(FILE *pOutput, const char *pszFile, size_t cchFile, int fDepenency)
{
    size_t cchWritten = 0;
    size_t off        = 0;
    while (off < cchFile)
    {
        char const ch = pszFile[off];
        switch (ch)
        {
            default:
                off++;
                break;

            /*
             * Escaped by slash, but any preceeding slashes must be escaped too.
             * A couple of characters are only escaped on one side of the ':'.
             */
            case '%': /* target side only */
            case '|': /* dependency side only */
                if (ch != (fDepenency ? '|' : '%'))
                {
                    off++;
                    break;
                }
                /* fall thru */
            case ' ':
            case '\t':
            case '#':
            case '=': /** @todo buggy GNU make handling */
            case ';': /** @todo buggy GNU make handling */
                if (cchWritten < off)
                    fwrite(&pszFile[cchWritten], off - cchWritten, 1, pOutput);
                if (off == 0 || pszFile[off - 1] != '\\')
                {
                    fputc('\\', pOutput);
                    cchWritten = off; /* We write the escaped character with the next bunch. */
                }
                else
                {
                    size_t cchSlashes = 1;
                    while (cchSlashes < off && pszFile[off - cchSlashes - 1] == '\\')
                        cchSlashes++;
                    fwrite(&pszFile[off - cchSlashes], cchSlashes, 1, pOutput);
                    cchWritten = off - 1; /* Write a preceeding slash and the escaped character with the next bunch. */
                }
                off += 1;
                break;

            /*
             * Escaped by doubling it.
             * Implemented by including in the pending writeout job as well as in the next one.
             */
            case '$':
                fwrite(&pszFile[cchWritten], off - cchWritten + 1, 1, pOutput);
                cchWritten = off++; /* write it again the next time */
                break;
        }
    }

    /* Remainder: */
    if (cchWritten < cchFile)
        fwrite(&pszFile[cchWritten], cchFile - cchWritten, 1, pOutput);
}


/**
 * Escapes all trailing trailing slashes in a filename that ends with such.
 */
static void depPrintTrailngSlashEscape(FILE *pOutput, const char *pszFilename, size_t cchFilename)
{
    size_t cchSlashes = 1;
    while (cchSlashes < cchFilename && pszFilename[cchFilename - cchSlashes - 1] == '\\')
        cchSlashes++;
    fwrite(&pszFilename[cchFilename - cchSlashes], cchSlashes, 1, pOutput);
}


/**
 * Prints the dependency chain.
 *
 * @param   pThis       The 'dep' instance.
 * @param   pOutput     Output stream.
 */
void depPrintChain(PDEPGLOBALS pThis, FILE *pOutput)
{
    static char const g_szEntryText[]     = " \\\n\t";
    static char const g_szTailText[]      = "\n\n";
    static char const g_szTailSlashText[] = " \\\n\n";
    PDEP              pDep;
    for (pDep = pThis->pDeps; pDep; pDep = pDep->pNext)
    {
        fwrite(g_szEntryText, sizeof(g_szEntryText) - 1, 1, pOutput);
        if (!pDep->fNeedsEscaping)
            fwrite(pDep->szFilename, pDep->cchFilename, 1, pOutput);
        else
            depEscapedWrite(pOutput, pDep->szFilename, pDep->cchFilename, 1 /*fDependency*/);
        if (pDep->fTrailingSlash)
        {   /* Escape only if more dependencies.  If last, we must add a line continuation or it won't work. */
            if (pDep->pNext)
                depPrintTrailngSlashEscape(pOutput, pDep->szFilename, pDep->cchFilename);
            else
            {
                fwrite(g_szTailSlashText, sizeof(g_szTailSlashText), 1, pOutput);
                return;
            }
        }
    }

    fwrite(g_szTailText, sizeof(g_szTailText) - 1, 1, pOutput);
}


/**
 * Prints the dependency chain with a preceeding target.
 *
 * @param   pThis           The 'dep' instance.
 * @param   pOutput         Output stream.
 * @param   pszTarget       The target filename.
 * @param   fEscapeTarget   Whether to consider escaping the target.
 */
void depPrintTargetWithDeps(PDEPGLOBALS pThis, FILE *pOutput, const char *pszTarget, int fEscapeTarget)
{
    static char const g_szSeparator[] = ":";
    size_t const cchTarget = strlen(pszTarget);
    if (!fEscapeTarget || !depNeedsEscaping(pszTarget, cchTarget, 0 /*fDependency*/))
        fwrite(pszTarget, cchTarget, 1, pOutput);
    else
        depEscapedWrite(pOutput, pszTarget, cchTarget, 0 /*fDependency*/);

    if (cchTarget == 0 || pszTarget[cchTarget - 1] != '\\')
    { /* likely */ }
    else
        depPrintTrailngSlashEscape(pOutput, pszTarget, cchTarget);
    fwrite(g_szSeparator, sizeof(g_szSeparator) - 1, 1, pOutput);

    depPrintChain(pThis, pOutput);
}


/**
 * Prints empty dependency stubs for all dependencies.
 *
 * @param   pThis       The 'dep' instance.
 * @param   pOutput     Output stream.
 */
void depPrintStubs(PDEPGLOBALS pThis, FILE *pOutput)
{
    static char g_szTailText[] = ":\n\n";
    PDEP pDep;
    for (pDep = pThis->pDeps; pDep; pDep = pDep->pNext)
    {
        if (!pDep->fNeedsEscaping && memchr(pDep->szFilename, '%', pDep->cchFilename) == 0)
            fwrite(pDep->szFilename, pDep->cchFilename, 1, pOutput);
        else
            depEscapedWrite(pOutput, pDep->szFilename, pDep->cchFilename, 0 /*fDependency*/);

        if (pDep->cchFilename == 0 || !pDep->fTrailingSlash)
        { /* likely */ }
        else
            depPrintTrailngSlashEscape(pOutput, pDep->szFilename, pDep->cchFilename);
        fwrite(g_szTailText, sizeof(g_szTailText) - 1, 1, pOutput);
    }
}


/* sdbm:
   This algorithm was created for sdbm (a public-domain reimplementation of
   ndbm) database library. it was found to do well in scrambling bits,
   causing better distribution of the keys and fewer splits. it also happens
   to be a good general hashing function with good distribution. the actual
   function is hash(i) = hash(i - 1) * 65599 + str[i]; what is included below
   is the faster version used in gawk. [there is even a faster, duff-device
   version] the magic constant 65599 was picked out of thin air while
   experimenting with different constants, and turns out to be a prime.
   this is one of the algorithms used in berkeley db (see sleepycat) and
   elsewhere. */
static unsigned sdbm(const char *str, size_t size)
{
    unsigned hash = 0;
    int c;

    while (size-- > 0 && (c = *(unsigned const char *)str++))
        hash = c + (hash << 6) + (hash << 16) - hash;

    return hash;
}


/**
 * Adds a dependency.
 *
 * @returns Pointer to the allocated dependency.
 * @param   pThis       The 'dep' instance.
 * @param   pszFilename     The filename. Does not need to be terminated.
 * @param   cchFilename     The length of the filename.
 */
PDEP depAdd(PDEPGLOBALS pThis, const char *pszFilename, size_t cchFilename)
{
    unsigned    uHash = sdbm(pszFilename, cchFilename);
    PDEP        pDep;
    PDEP        pDepPrev;

    /*
     * Check if we've already got this one.
     */
    pDepPrev = NULL;
    for (pDep = pThis->pDeps; pDep; pDepPrev = pDep, pDep = pDep->pNext)
        if (    pDep->uHash == uHash
            &&  pDep->cchFilename == cchFilename
            &&  !memcmp(pDep->szFilename, pszFilename, cchFilename))
            return pDep;

    /*
     * Add it.
     */
    pDep = (PDEP)malloc(sizeof(*pDep) + cchFilename);
    if (!pDep)
    {
        fprintf(stderr, "\nOut of memory! (requested %lx bytes)\n\n",
                (unsigned long)(sizeof(*pDep) + cchFilename));
        exit(1);
    }

    pDep->cchFilename = cchFilename;
    memcpy(pDep->szFilename, pszFilename, cchFilename);
    pDep->szFilename[cchFilename] = '\0';
    pDep->fNeedsEscaping = depNeedsEscaping(pszFilename, cchFilename, 1 /*fDependency*/);
    pDep->fTrailingSlash = cchFilename > 0 && pszFilename[cchFilename - 1] == '\\';
    pDep->uHash = uHash;

    if (pDepPrev)
    {
        pDep->pNext = pDepPrev->pNext;
        pDepPrev->pNext = pDep;
    }
    else
    {
        pDep->pNext = pThis->pDeps;
        pThis->pDeps = pDep;
    }
    return pDep;
}


/**
 * Performs a hexdump.
 */
void depHexDump(const KU8 *pb, size_t cb, size_t offBase)
{
    const unsigned      cchWidth = 16;
    size_t              off = 0;
    while (off < cb)
    {
        unsigned i;
        printf("%s%0*lx %04lx:", off ? "\n" : "", (int)sizeof(pb) * 2,
               (unsigned long)offBase + (unsigned long)off, (unsigned long)off);
        for (i = 0; i < cchWidth && off + i < cb ; i++)
            printf(off + i < cb ? !(i & 7) && i ? "-%02x" : " %02x" : "   ", pb[i]);

        while (i++ < cchWidth)
                printf("   ");
        printf(" ");

        for (i = 0; i < cchWidth && off + i < cb; i++)
        {
            const KU8 u8 = pb[i];
            printf("%c", u8 < 127 && u8 >= 32 ? u8 : '.');
        }
        off += cchWidth;
        pb  += cchWidth;
    }
    printf("\n");
}


/**
 * Reads the file specified by the pInput file stream into memory.
 *
 * @returns The address of the memory mapping on success. This must be
 *          freed by calling depFreeFileMemory.
 *
 * @param   pInput      The file stream to load or map into memory.
 * @param   pcbFile     Where to return the mapping (file) size.
 * @param   ppvOpaque   Opaque data when mapping, otherwise NULL.
 */
void *depReadFileIntoMemory(FILE *pInput, size_t *pcbFile, void **ppvOpaque)
{
    void       *pvFile;
    long        cbFile;

    /*
     * Figure out file size.
     */
#if defined(_MSC_VER)
    cbFile = _filelength(fileno(pInput));
    if (cbFile < 0)
#else
    if (    fseek(pInput, 0, SEEK_END) < 0
        ||  (cbFile = ftell(pInput)) < 0
        ||  fseek(pInput, 0, SEEK_SET))
#endif
    {
        fprintf(stderr, "kDep: error: Failed to determin file size.\n");
        return NULL;
    }
    if (pcbFile)
        *pcbFile = cbFile;

    /*
     * Try mmap first.
     */
#ifdef USE_WIN_MMAP
    {
        HANDLE hMapObj = CreateFileMapping((HANDLE)_get_osfhandle(fileno(pInput)),
                                           NULL, PAGE_READONLY, 0, cbFile, NULL);
        if (hMapObj != NULL)
        {
            pvFile = MapViewOfFile(hMapObj, FILE_MAP_READ, 0, 0, cbFile);
            if (pvFile)
            {
                *ppvOpaque = hMapObj;
                return pvFile;
            }
            fprintf(stderr, "kDep: warning: MapViewOfFile failed, %d.\n", GetLastError());
            CloseHandle(hMapObj);
        }
        else
            fprintf(stderr, "kDep: warning: CreateFileMapping failed, %d.\n", GetLastError());
    }

#endif

    /*
     * Allocate memory and read the file.
     */
    pvFile = malloc(cbFile + 1);
    if (pvFile)
    {
        if (fread(pvFile, cbFile, 1, pInput))
        {
            ((KU8 *)pvFile)[cbFile] = '\0';
            *ppvOpaque = NULL;
            return pvFile;
        }
        fprintf(stderr, "kDep: error: Failed to read %ld bytes.\n", cbFile);
        free(pvFile);
    }
    else
        fprintf(stderr, "kDep: error: Failed to allocate %ld bytes (file mapping).\n", cbFile);
    return NULL;
}


/**
 * Free resources allocated by depReadFileIntoMemory.
 *
 * @param   pvFile      The address of the memory mapping.
 * @param   pvOpaque    The opaque value returned together with the mapping.
 */
void depFreeFileMemory(void *pvFile, void *pvOpaque)
{
#if defined(USE_WIN_MMAP)
    if (pvOpaque)
    {
        UnmapViewOfFile(pvFile);
        CloseHandle(pvOpaque);
        return;
    }
#endif
    free(pvFile);
}

