/*
 *  BATCH.C - batch file processor for CMD.EXE.
 *
 *
 *  History:
 *
 *    ??/??/?? (Evan Jeffrey)
 *        started.
 *
 *    15 Jul 1995 (Tim Norman)
 *        modes and bugfixes.
 *
 *    08 Aug 1995 (Matt Rains)
 *        i have cleaned up the source code. changes now bring this
 *        source into guidelines for recommended programming practice.
 *
 *        i have added some constants to help making changes easier.
 *
 *    29 Jan 1996 (Steffan Kaiser)
 *        made a few cosmetic changes
 *
 *    05 Feb 1996 (Tim Norman)
 *        changed to comply with new first/rest calling scheme
 *
 *    14 Jun 1997 (Steffen Kaiser)
 *        bug fixes.  added error level expansion %?.  ctrl-break handling
 *
 *    16 Jul 1998 (Hans B Pufal)
 *        Totally reorganised in conjunction with COMMAND.C (cf) to
 *        implement proper BATCH file nesting and other improvements.
 *
 *    16 Jul 1998 (John P Price <linux-guru@gcfl.net>)
 *        Separated commands into individual files.
 *
 *    19 Jul 1998 (Hans B Pufal) [HBP_001]
 *        Preserve state of echo flag across batch calls.
 *
 *    19 Jul 1998 (Hans B Pufal) [HBP_002]
 *        Implementation of FOR command
 *
 *    20-Jul-1998 (John P Price <linux-guru@gcfl.net>)
 *        added error checking after cmd_alloc calls
 *
 *    27-Jul-1998 (John P Price <linux-guru@gcfl.net>)
 *        added config.h include
 *
 *    02-Aug-1998 (Hans B Pufal) [HBP_003]
 *        Fixed bug in ECHO flag restoration at exit from batch file
 *
 *    26-Jan-1999 Eric Kohl
 *        Replaced CRT io functions by Win32 io functions.
 *        Unicode safe!
 *
 *    23-Feb-2001 (Carl Nettelblad <cnettel@hem.passagen.es>)
 *        Fixes made to get "for" working.
 *
 *    02-Apr-2005 (Magnus Olsen <magnus@greatlord.com>)
 *        Remove all hardcoded strings in En.rc
 */

#include "precomp.h"

/* The stack of current batch contexts.
 * NULL when no batch is active.
 */
PBATCH_CONTEXT bc = NULL;

BOOL bEcho = TRUE;  /* The echo flag */

/* Buffer for reading Batch file lines */
TCHAR textline[BATCH_BUFFSIZE];


/*
 * Returns a pointer to the n'th parameter of the current batch file.
 * If no such parameter exists returns pointer to empty string.
 * If no batch file is current, returns NULL
 *
 */
LPTSTR FindArg(TCHAR Char, BOOL *IsParam0)
{
    LPTSTR pp;
    INT n = Char - _T('0');

    TRACE ("FindArg: (%d)\n", n);

    if (n < 0 || n > 9)
        return NULL;

    n = bc->shiftlevel[n];
    *IsParam0 = (n == 0);
    pp = bc->params;

    /* Step up the strings till we reach the end */
    /* or the one we want */
    while (*pp && n--)
        pp += _tcslen (pp) + 1;

    return pp;
}


/*
 * Batch_params builds a parameter list in newly allocated memory.
 * The parameters consist of null terminated strings with a final
 * NULL character signalling the end of the parameters.
 *
 */
static LPTSTR BatchParams(LPTSTR s1, LPTSTR s2)
{
    LPTSTR dp = (LPTSTR)cmd_alloc((_tcslen(s1) + _tcslen(s2) + 3) * sizeof (TCHAR));

    /* JPP 20-Jul-1998 added error checking */
    if (dp == NULL)
    {
        WARN("Cannot allocate memory for dp!\n");
        error_out_of_memory();
        return NULL;
    }

    if (s1 && *s1)
    {
        s1 = _stpcpy (dp, s1);
        *s1++ = _T('\0');
    }
    else
        s1 = dp;

    while (*s2)
    {
        BOOL inquotes = FALSE;

        /* Find next parameter */
        while (_istspace(*s2) || (*s2 && _tcschr(STANDARD_SEPS, *s2)))
            s2++;
        if (!*s2)
            break;

        /* Copy it */
        do
        {
            if (!inquotes && (_istspace(*s2) || _tcschr(STANDARD_SEPS, *s2)))
                break;
            inquotes ^= (*s2 == _T('"'));
            *s1++ = *s2++;
        } while (*s2);
        *s1++ = _T('\0');
    }

    *s1 = _T('\0');

    return dp;
}

/*
 * Free the allocated memory of a batch file.
 */
static VOID ClearBatch(VOID)
{
    TRACE("ClearBatch  mem = %08x ; free = %d\n", bc->mem, bc->memfree);

    if (bc->mem && bc->memfree)
        cmd_free(bc->mem);

    if (bc->raw_params)
        cmd_free(bc->raw_params);

    if (bc->params)
        cmd_free(bc->params);
}

/*
 * If a batch file is current, exits it, freeing the context block and
 * chaining back to the previous one.
 *
 * If no new batch context is found, sets ECHO back ON.
 *
 * If the parameter is non-null or not empty, it is printed as an exit
 * message
 */

VOID ExitBatch(VOID)
{
    ClearBatch();

    TRACE("ExitBatch\n");

    UndoRedirection(bc->RedirList, NULL);
    FreeRedirection(bc->RedirList);

    /* Preserve echo state across batch calls */
    bEcho = bc->bEcho;

    while (bc->setlocal)
        cmd_endlocal(_T(""));

    bc = bc->prev;

#if 0
    /* Do not process any more parts of a compound command */
    bc->current = NULL;
#endif

    /* If there is no more batch contexts, notify the signal handler */
    if (!bc)
        CheckCtrlBreak(BREAK_OUTOFBATCH);
}

/*
 * Exit all the nested batch calls.
 */
VOID ExitAllBatches(VOID)
{
    while (bc)
        ExitBatch();
}

/*
 * Load batch file into memory.
 */
static void BatchFile2Mem(HANDLE hBatchFile)
{
    TRACE("BatchFile2Mem()\n");

    bc->memsize = GetFileSize(hBatchFile, NULL);
    bc->mem     = (char *)cmd_alloc(bc->memsize+1);     /* 1 extra for '\0' */

    /* if memory is available, read it in and close the file */
    if (bc->mem != NULL)
    {
        TRACE ("BatchFile2Mem memory %08x - %08x\n",bc->mem,bc->memsize);
        SetFilePointer (hBatchFile, 0, NULL, FILE_BEGIN);
        ReadFile(hBatchFile, (LPVOID)bc->mem, bc->memsize,  &bc->memsize, NULL);
        bc->mem[bc->memsize]='\0';  /* end this, so you can dump it as a string */
        bc->memfree=TRUE;           /* this one needs to be freed */
    }
    else
    {
        bc->memsize=0;              /* this will prevent mem being accessed */
        bc->memfree=FALSE;
    }
    bc->mempos = 0;                 /* set position to the start */
}

/*
 * Start batch file execution.
 *
 * The firstword parameter is the full filename of the batch file.
 */
INT Batch(LPTSTR fullname, LPTSTR firstword, LPTSTR param, PARSED_COMMAND *Cmd)
{
    INT ret = 0;
    INT i;
    HANDLE hFile = NULL;
    BOOL bSameFn = FALSE;
    BATCH_CONTEXT new;
    PFOR_CONTEXT saved_fc;

    SetLastError(0);
    if (bc && bc->mem)
    {
        TCHAR fpname[MAX_PATH];
        GetFullPathName(fullname, ARRAYSIZE(fpname), fpname, NULL);
        if (_tcsicmp(bc->BatchFilePath, fpname) == 0)
            bSameFn = TRUE;
    }
    TRACE("Batch(\'%s\', \'%s\', \'%s\')  bSameFn = %d\n",
        debugstr_aw(fullname), debugstr_aw(firstword), debugstr_aw(param), bSameFn);

    if (!bSameFn)
    {
        hFile = CreateFile(fullname, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, NULL,
                           OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL |
                           FILE_FLAG_SEQUENTIAL_SCAN, NULL);

        if (hFile == INVALID_HANDLE_VALUE)
        {
            ConErrResPuts(STRING_BATCH_ERROR);
            return 1;
        }
    }

    if (bc != NULL && Cmd == bc->current)
    {
        /* Then we are transferring to another batch */
        ClearBatch();
        AddBatchRedirection(&Cmd->Redirections);
    }
    else
    {
        struct _SETLOCAL *setlocal = NULL;

        if (Cmd == NULL)
        {
            /* This is a CALL. CALL will set errorlevel to our return value, so
             * in order to keep the value of errorlevel unchanged in the case
             * of calling an empty batch file, we must return that same value. */
            ret = nErrorLevel;
        }
        else if (bc)
        {
            /* If a batch file runs another batch file as part of a compound command
             * (e.g. "x.bat & somethingelse") then the first file gets terminated. */

            /* Get its SETLOCAL stack so it can be migrated to the new context */
            setlocal = bc->setlocal;
            bc->setlocal = NULL;
            ExitBatch();
        }

        /* Create a new context. This function will not
         * return until this context has been exited */
        new.prev = bc;
        /* copy some fields in the new structure if it is the same file */
        if (bSameFn)
        {
            new.mem     = bc->mem;
            new.memsize = bc->memsize;
            new.mempos  = 0;
            new.memfree = FALSE;    /* don't free this, being used before this */
        }
        bc = &new;
        bc->RedirList = NULL;
        bc->setlocal = setlocal;
    }

    GetFullPathName(fullname, ARRAYSIZE(bc->BatchFilePath), bc->BatchFilePath, NULL);

    /* If a new batch file, load it into memory and close the file */
    if (!bSameFn)
    {
        BatchFile2Mem(hFile);
        CloseHandle(hFile);
    }

    bc->mempos = 0;    /* Go to the beginning of the batch file */
    bc->bEcho = bEcho; /* Preserve echo across batch calls */
    for (i = 0; i < 10; i++)
        bc->shiftlevel[i] = i;

    /* Parse the parameters and make a raw copy of them without modifications */
    bc->params = BatchParams(firstword, param);
    bc->raw_params = cmd_dup(param);
    if (bc->raw_params == NULL)
    {
        error_out_of_memory();
        return 1;
    }

    /* Check if this is a "CALL :label" */
    if (*firstword == _T(':'))
        ret = cmd_goto(firstword);

    /* If we are calling from inside a FOR, hide the FOR variables */
    saved_fc = fc;
    fc = NULL;

    /* If we have created a new context, don't return
     * until this batch file has completed. */
    while (bc == &new && !bExit)
    {
        Cmd = ParseCommand(NULL);
        if (!Cmd)
        {
            if (!bParseError)
                continue;

            /* Echo the pre-parsed batch file line on error */
            if (bEcho && !bDisableBatchEcho)
            {
                if (!bIgnoreEcho)
                    ConOutChar(_T('\n'));
                PrintPrompt();
                ConOutPuts(ParseLine);
                ConOutChar(_T('\n'));
            }
            /* Stop all execution */
            ExitAllBatches();
            ret = 1;
            break;
        }

        /* JPP 19980807 */
        /* Echo the command and execute it */
        bc->current = Cmd;
        ret = ExecuteCommandWithEcho(Cmd);
        FreeCommand(Cmd);
    }

    /* Restore the FOR variables */
    fc = saved_fc;

    /* Always return the last command's return code */
    TRACE("Batch: returns %d\n", ret);
    return ret;
}

VOID AddBatchRedirection(REDIRECTION **RedirList)
{
    REDIRECTION **ListEnd;

    /* Prepend the list to the batch context's list */
    ListEnd = RedirList;
    while (*ListEnd)
        ListEnd = &(*ListEnd)->Next;
    *ListEnd = bc->RedirList;
    bc->RedirList = *RedirList;

    /* Null out the pointer so that the list will not be cleared prematurely.
     * These redirections should persist until the batch file exits. */
    *RedirList = NULL;
}

/*
 *   Read a single line from the batch file from the current batch/memory position.
 *   Almost a copy of FileGetString with same UNICODE handling
 */
BOOL BatchGetString(LPTSTR lpBuffer, INT nBufferLength)
{
    LPSTR lpString;
    INT len = 0;
#ifdef _UNICODE
    lpString = cmd_alloc(nBufferLength);
    if (!lpString)
    {
        WARN("Cannot allocate memory for lpString\n");
        error_out_of_memory();
        return FALSE;
    }
#else
    lpString = lpBuffer;
#endif
    /* read all chars from memory until a '\n' is encountered */
    if (bc->mem)
    {
        for (; (bc->mempos < bc->memsize  &&  len < (nBufferLength-1)); len++)
        { 
            lpString[len] = bc->mem[bc->mempos++];
            if (lpString[len] == '\n' )
            {
                len++;
                break;
            }
        }
    }

    if (!len)
    {
#ifdef _UNICODE
        cmd_free(lpString);
#endif
        return FALSE;
    }

    lpString[len++] = '\0';
#ifdef _UNICODE
    MultiByteToWideChar(OutputCodePage, 0, lpString, -1, lpBuffer, len);
    cmd_free(lpString);
#endif
    return TRUE;
}

/*
 * Read and return the next executable line form the current batch file
 *
 * If no batch file is current or no further executable lines are found
 * return NULL.
 *
 * Set eflag to 0 if line is not to be echoed else 1
 */
LPTSTR ReadBatchLine(VOID)
{
    TRACE("ReadBatchLine()\n");

    /* User halt */
    if (CheckCtrlBreak(BREAK_BATCHFILE))
    {
        ExitAllBatches();
        return NULL;
    }

    if (!BatchGetString(textline, ARRAYSIZE(textline) - 1))
    {
        TRACE("ReadBatchLine(): Reached EOF!\n");
        /* End of file */
        ExitBatch();
        return NULL;
    }

    TRACE("ReadBatchLine(): textline: \'%s\'\n", debugstr_aw(textline));

    if (textline[_tcslen(textline) - 1] != _T('\n'))
        _tcscat(textline, _T("\n"));

    return textline;
}

/* EOF */