/*====================================================================*
 -  Copyright (C) 2001 Leptonica.  All rights reserved.
 -
 -  Redistribution and use in source and binary forms, with or without
 -  modification, are permitted provided that the following conditions
 -  are met:
 -  1. Redistributions of source code must retain the above copyright
 -     notice, this list of conditions and the following disclaimer.
 -  2. Redistributions in binary form must reproduce the above
 -     copyright notice, this list of conditions and the following
 -     disclaimer in the documentation and/or other materials
 -     provided with the distribution.
 -
 -  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 -  ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 -  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 -  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL ANY
 -  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 -  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 -  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 -  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 -  OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 -  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 -  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *====================================================================*/


/*
 *  regutils.c
 *
 *       Regression test utilities
 *           l_int32    regTestSetup()
 *           l_int32    regTestCleanup()
 *           l_int32    regTestCompareValues()
 *           l_int32    regTestCompareStrings()
 *           l_int32    regTestComparePix()
 *           l_int32    regTestCompareSimilarPix()
 *           l_int32    regTestCheckFile()
 *           l_int32    regTestCompareFiles()
 *           l_int32    regTestWritePixAndCheck()
 *
 *       Static function
 *           char      *getRootNameFromArgv0()
 *
 *  See regutils.h for how to use this.  Here is a minimal setup:
 *
 *  main(int argc, char **argv) {
 *  ...
 *  L_REGPARAMS  *rp;
 *
 *      if (regTestSetup(argc, argv, &rp))
 *          return 1;
 *      ...
 *      regTestWritePixAndCheck(rp, pix, IFF_PNG);  // 0
 *      ...
 *      return regTestCleanup(rp);
 *  }
 */

#include <string.h>
#include "allheaders.h"

extern l_int32 NumImageFileFormatExtensions;
extern const char *ImageFileFormatExtensions[];

static char *getRootNameFromArgv0(const char *argv0);

/*--------------------------------------------------------------------*
 *                      Regression test utilities                     *
 *--------------------------------------------------------------------*/
/*!
 *  regTestSetup()
 *
 *      Input:  argc (from invocation; can be either 1 or 2)
 *              argv (to regtest: @argv[1] is one of these:
 *                    "generate", "compare", "display")
 *              &rp (<return> all regression params)
 *      Return: 0 if OK, 1 on error
 *
 *  Notes:
 *      (1) Call this function with the args to the reg test.
 *          There are three cases:
 *          Case 1:
 *              There is either only one arg, or the second arg is "compare".
 *              This is the mode in which you run a regression test
 *              (or a set of them), looking for failures and logging
 *              the results to a file.  The output, which includes
 *              logging of all reg test failures plus a SUCCESS or
 *              FAILURE summary for each test, is appended to the file
 *              "/tmp/reg_results.txt.  For this case, as in Case 2,
 *              the display field in rp is set to FALSE, preventing
 *              image display.
 *          Case 2:
 *              The second arg is "generate".  This will cause
 *              generation of new golden files for the reg test.
 *              The results of the reg test are not recorded, and
 *              the display field in rp is set to FALSE.
 *          Case 3:
 *              The second arg is "display".  The test will run and
 *              files will be written.  Comparisons with golden files
 *              will not be carried out, so the only notion of success
 *              or failure is with tests that do not involve golden files.
 *              The display field in rp is TRUE, and this is used by
 *              pixDisplayWithTitle().
 *      (2) See regutils.h for examples of usage.
 */
l_int32
regTestSetup(l_int32        argc,
             char         **argv,
             L_REGPARAMS  **prp)
{
char         *testname, *vers;
char          errormsg[64];
L_REGPARAMS  *rp;

    PROCNAME("regTestSetup");

    if (argc != 1 && argc != 2) {
        snprintf(errormsg, sizeof(errormsg),
            "Syntax: %s [ [generate] | compare | display ]", argv[0]);
        return ERROR_INT(errormsg, procName, 1);
    }

    if ((testname = getRootNameFromArgv0(argv[0])) == NULL)
        return ERROR_INT("invalid root", procName, 1);

    if ((rp = (L_REGPARAMS *)CALLOC(1, sizeof(L_REGPARAMS))) == NULL)
        return ERROR_INT("rp not made", procName, 1);
    *prp = rp;
    rp->testname = testname;
    rp->index = -1;  /* increment before each test */

        /* Initialize to true.  A failure in any test is registered
         * as a failure of the regression test. */
    rp->success = TRUE;

        /* Make sure the regout subdirectory exists */
    lept_mkdir("regout");

        /* Only open a stream to a temp file for the 'compare' case */
    if (argc == 1 || !strcmp(argv[1], "compare")) {
        rp->mode = L_REG_COMPARE;
        rp->tempfile = genPathname("/tmp/regout", "regtest_output.txt");
        rp->fp = fopenWriteStream(rp->tempfile, "wb");
        if (rp->fp == NULL) {
            rp->success = FALSE;
            return ERROR_INT("stream not opened for tempfile", procName, 1);
        }
    } else if (!strcmp(argv[1], "generate")) {
        rp->mode = L_REG_GENERATE;
        lept_mkdir("golden");
    } else if (!strcmp(argv[1], "display")) {
        rp->mode = L_REG_DISPLAY;
        rp->display = TRUE;
    } else {
        FREE(rp);
        snprintf(errormsg, sizeof(errormsg),
            "Syntax: %s [ [generate] | compare | display ]", argv[0]);
        return ERROR_INT(errormsg, procName, 1);
    }

        /* Print out test name and both the leptonica and
         * image libarary versions */
    fprintf(stderr, "\n################   %s_reg   ###############\n",
            rp->testname);
    vers = getLeptonicaVersion();
    fprintf(stderr, "%s\n", vers);
    FREE(vers);
    vers = getImagelibVersions();
    fprintf(stderr, "%s\n", vers);
    FREE(vers);

    rp->tstart = startTimerNested();
    return 0;
}


/*!
 *  regTestCleanup()
 *
 *      Input:  rp (regression test parameters)
 *      Return: 0 if OK, 1 on error
 *
 *  Notes:
 *      (1) This copies anything written to the temporary file to the
 *          output file /tmp/reg_results.txt.
 */
l_int32
regTestCleanup(L_REGPARAMS  *rp)
{
char     result[512];
char    *results_file;  /* success/failure output in 'compare' mode */
char    *text, *message;
l_int32  retval;
size_t   nbytes;

    PROCNAME("regTestCleanup");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);

    fprintf(stderr, "Time: %7.3f sec\n", stopTimerNested(rp->tstart));
    fprintf(stderr, "################################################\n");

        /* If generating golden files or running in display mode, release rp */
    if (!rp->fp) {
        FREE(rp->testname);
        FREE(rp->tempfile);
        FREE(rp);
        return 0;
    }

        /* Compare mode: read back data from temp file */
    fclose(rp->fp);
    text = (char *)l_binaryRead(rp->tempfile, &nbytes);
    FREE(rp->tempfile);
    if (!text) {
        rp->success = FALSE;
        FREE(rp);
        return ERROR_INT("text not returned", procName, 1);
    }

        /* Prepare result message */
    if (rp->success)
        snprintf(result, sizeof(result), "SUCCESS: %s_reg\n", rp->testname);
    else
        snprintf(result, sizeof(result), "FAILURE: %s_reg\n", rp->testname);
    message = stringJoin(text, result);
    FREE(text);
    results_file = genPathname("/tmp", "reg_results.txt");
    fileAppendString(results_file, message);
    retval = (rp->success) ? 0 : 1;
    FREE(results_file);
    FREE(message);

    FREE(rp->testname);
    FREE(rp);
    return retval;
}


/*!
 *  regTestCompareValues()
 *
 *      Input:  rp (regtest parameters)
 *              val1 (typ. the golden value)
 *              val2 (typ. the value computed)
 *              delta (allowed max absolute difference)
 *      Return: 0 if OK, 1 on error (a failure in comparison is not an error)
 */
l_int32
regTestCompareValues(L_REGPARAMS  *rp,
                     l_float32     val1,
                     l_float32     val2,
                     l_float32     delta)
{
l_float32  diff;

    PROCNAME("regTestCompareValues");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);

    rp->index++;
    diff = L_ABS(val2 - val1);

        /* Record on failure */
    if (diff > delta) {
        if (rp->fp) {
            fprintf(rp->fp,
                    "Failure in %s_reg: value comparison for index %d\n"
                    "difference = %f but allowed delta = %f\n",
                    rp->testname, rp->index, diff, delta);
        }
        fprintf(stderr,
                    "Failure in %s_reg: value comparison for index %d\n"
                    "difference = %f but allowed delta = %f\n",
                    rp->testname, rp->index, diff, delta);
        rp->success = FALSE;
    }
    return 0;
}


/*!
 *  regTestCompareStrings()
 *
 *      Input:  rp (regtest parameters)
 *              string1 (typ. the expected string)
 *              bytes1 (size of string1)
 *              string2 (typ. the computed string)
 *              bytes2 (size of string2)
 *      Return: 0 if OK, 1 on error (a failure in comparison is not an error)
 */
l_int32
regTestCompareStrings(L_REGPARAMS  *rp,
                      l_uint8      *string1,
                      size_t        bytes1,
                      l_uint8      *string2,
                      size_t        bytes2)
{
l_int32  i, fail;
char     buf[256];

    PROCNAME("regTestCompareValues");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);

    rp->index++;
    fail = FALSE;
    if (bytes1 != bytes2) fail = TRUE;
    if (fail == FALSE) {
        for (i = 0; i < bytes1; i++) {
            if (string1[i] != string2[i]) {
                fail = TRUE;
                break;
            }
        }
    }

        /* Output on failure */
    if (fail == TRUE) {
            /* Write the two strings to file */
        snprintf(buf, sizeof(buf), "/tmp/regout/string1_%d_%lu", rp->index,
                 (unsigned long)bytes1);
        l_binaryWrite(buf, "w", string1, bytes1);
        snprintf(buf, sizeof(buf), "/tmp/regout/string2_%d_%lu", rp->index,
                 (unsigned long)bytes2);
        l_binaryWrite(buf, "w", string2, bytes2);

            /* Report comparison failure */
        snprintf(buf, sizeof(buf), "/tmp/regout/string*_%d_*", rp->index);
        if (rp->fp) {
            fprintf(rp->fp,
                    "Failure in %s_reg: string comp for index %d; "
                    "written to %s\n", rp->testname, rp->index, buf);
        }
        fprintf(stderr,
                    "Failure in %s_reg: string comp for index %d; "
                    "written to %s\n", rp->testname, rp->index, buf);
        rp->success = FALSE;
    }
    return 0;
}


/*!
 *  regTestComparePix()
 *
 *      Input:  rp (regtest parameters)
 *              pix1, pix2 (to be tested for equality)
 *      Return: 0 if OK, 1 on error (a failure in comparison is not an error)
 *
 *  Notes:
 *      (1) This function compares two pix for equality.  On failure,
 *          this writes to stderr.
 */
l_int32
regTestComparePix(L_REGPARAMS  *rp,
                  PIX          *pix1,
                  PIX          *pix2)
{
l_int32  same;

    PROCNAME("regTestComparePix");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);
    if (!pix1 || !pix2) {
        rp->success = FALSE;
        return ERROR_INT("pix1 and pix2 not both defined", procName, 1);
    }

    rp->index++;
    pixEqual(pix1, pix2, &same);

        /* Record on failure */
    if (!same) {
        if (rp->fp) {
            fprintf(rp->fp, "Failure in %s_reg: pix comparison for index %d\n",
                    rp->testname, rp->index);
        }
        fprintf(stderr, "Failure in %s_reg: pix comparison for index %d\n",
                rp->testname, rp->index);
        rp->success = FALSE;
    }
    return 0;
}


/*!
 *  regTestCompareSimilarPix()
 *
 *      Input:  rp (regtest parameters)
 *              pix1, pix2 (to be tested for near equality)
 *              mindiff (minimum pixel difference to be counted; > 0)
 *              maxfract (maximum fraction of pixels allowed to have
 *                        diff greater than or equal to mindiff)
 *              printstats (use 1 to print normalized histogram to stderr)
 *      Return: 0 if OK, 1 on error (a failure in similarity comparison
 *              is not an error)
 *
 *  Notes:
 *      (1) This function compares two pix for near equality.  On failure,
 *          this writes to stderr.
 *      (2) The pix are similar if the fraction of non-conforming pixels
 *          does not exceed @maxfract.  Pixels are non-conforming if
 *          the difference in pixel values equals or exceeds @mindiff.
 *          Typical values might be @mindiff = 15 and @maxfract = 0.01.
 *      (3) The input images must have the same size and depth.  The
 *          pixels for comparison are typically subsampled from the images.
 *      (4) Normally, use @printstats = 0.  In debugging mode, to see
 *          the relation between @mindiff and the minimum value of
 *          @maxfract for success, set this to 1.
 */
l_int32
regTestCompareSimilarPix(L_REGPARAMS  *rp,
                         PIX          *pix1,
                         PIX          *pix2,
                         l_int32       mindiff,
                         l_float32     maxfract,
                         l_int32       printstats)
{
l_int32  w, h, factor, similar;

    PROCNAME("regTestCompareSimilarPix");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);
    if (!pix1 || !pix2) {
        rp->success = FALSE;
        return ERROR_INT("pix1 and pix2 not both defined", procName, 1);
    }

    rp->index++;
    pixGetDimensions(pix1, &w, &h, NULL);
    factor = L_MAX(w, h) / 400;
    factor = L_MAX(1, L_MIN(factor, 4));   /* between 1 and 4 */
    pixTestForSimilarity(pix1, pix2, factor, mindiff, maxfract, 0.0,
                         &similar, printstats);

        /* Record on failure */
    if (!similar) {
        if (rp->fp) {
            fprintf(rp->fp,
                    "Failure in %s_reg: pix similarity comp for index %d\n",
                    rp->testname, rp->index);
        }
        fprintf(stderr, "Failure in %s_reg: pix similarity comp for index %d\n",
                rp->testname, rp->index);
        rp->success = FALSE;
    }
    return 0;
}


/*!
 *  regTestCheckFile()
 *
 *      Input:  rp (regtest parameters)
 *              localname (name of output file from reg test)
 *      Return: 0 if OK, 1 on error (a failure in comparison is not an error)
 *
 *  Notes:
 *      (1) This function does one of three things, depending on the mode:
 *           * "generate": makes a "golden" file as a copy @localname.
 *           * "compare": compares @localname contents with the golden file
 *           * "display": makes the @localname file but does no comparison
 *      (2) The canonical format of the golden filenames is:
 *            /tmp/golden/<root of main name>_golden.<index>.<ext of localname>
 *          e.g.,
 *             /tmp/golden/maze_golden.0.png
 *          It is important to add an extension to the local name, because
 *          the extension is added to the name of the golden file.
 */
l_int32
regTestCheckFile(L_REGPARAMS  *rp,
                 const char   *localname)
{
char    *ext;
char     namebuf[256];
l_int32  ret, same, format;
PIX     *pix1, *pix2;

    PROCNAME("regTestCheckFile");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);
    if (!localname) {
        rp->success = FALSE;
        return ERROR_INT("local name not defined", procName, 1);
    }
    if (rp->mode != L_REG_GENERATE && rp->mode != L_REG_COMPARE &&
        rp->mode != L_REG_DISPLAY) {
        rp->success = FALSE;
        return ERROR_INT("invalid mode", procName, 1);
    }
    rp->index++;

        /* If display mode, no generation and no testing */
    if (rp->mode == L_REG_DISPLAY) return 0;

        /* Generate the golden file name; used in 'generate' and 'compare' */
    splitPathAtExtension(localname, NULL, &ext);
    snprintf(namebuf, sizeof(namebuf), "/tmp/golden/%s_golden.%02d%s",
             rp->testname, rp->index, ext);
    FREE(ext);

        /* Generate mode.  No testing. */
    if (rp->mode == L_REG_GENERATE) {
            /* Save the file as a golden file */
        ret = fileCopy(localname, namebuf);
#if 0       /* Enable for details on writing of golden files */
        if (!ret) {
            char *local = genPathname(localname, NULL);
            char *golden = genPathname(namebuf, NULL);
            L_INFO("Copy: %s to %s\n", procName, local, golden);
            FREE(local);
            FREE(golden);
        }
#endif
        return ret;
    }

        /* Compare mode: test and record on failure.  GIF compression
         * is lossless for images with up to 8 bpp (but not for RGB
         * because it must generate a 256 color palette).  Although
         * the read/write cycle for GIF is idempotent in the image
         * pixels for bpp <= 8, it is not idempotent in the actual
         * file bytes.  Tests comparing file bytes before and after
         * a GIF read/write cycle will fail.  So for GIF we uncompress
         * the two images and compare the actual pixels.  From my tests,
         * PNG, in addition to being lossless, is idempotent in file
         * bytes on read/write, so comparing the pixels is not necessary.
         * (It also increases the regression test time by an an average
         * of about 8%.)  JPEG is lossy and not idempotent in the image
         * pixels, so no tests are constructed that would require it. */
    findFileFormat(localname, &format);
    if (format == IFF_GIF) {
        same = 0;
        pix1 = pixRead(localname);
        pix2 = pixRead(namebuf);
        pixEqual(pix1, pix2, &same);
        pixDestroy(&pix1);
        pixDestroy(&pix2);
    } else {
        filesAreIdentical(localname, namebuf, &same);
    }
    if (!same) {
        fprintf(rp->fp, "Failure in %s_reg, index %d: comparing %s with %s\n",
                rp->testname, rp->index, localname, namebuf);
        fprintf(stderr, "Failure in %s_reg, index %d: comparing %s with %s\n",
                rp->testname, rp->index, localname, namebuf);
        rp->success = FALSE;
    }

    return 0;
}


/*!
 *  regTestCompareFiles()
 *
 *      Input:  rp (regtest parameters)
 *              index1 (of one output file from reg test)
 *              index2 (of another output file from reg test)
 *      Return: 0 if OK, 1 on error (a failure in comparison is not an error)
 *
 *  Notes:
 *      (1) This only does something in "compare" mode.
 *      (2) The canonical format of the golden filenames is:
 *            /tmp/golden/<root of main name>_golden.<index>.<ext of localname>
 *          e.g.,
 *            /tmp/golden/maze_golden.0.png
 */
l_int32
regTestCompareFiles(L_REGPARAMS  *rp,
                    l_int32       index1,
                    l_int32       index2)
{
char    *name1, *name2;
char     namebuf[256];
l_int32  same;
SARRAY  *sa;

    PROCNAME("regTestCompareFiles");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);
    if (index1 < 0 || index2 < 0) {
        rp->success = FALSE;
        return ERROR_INT("index1 and/or index2 is negative", procName, 1);
    }
    if (index1 == index2) {
        rp->success = FALSE;
        return ERROR_INT("index1 must differ from index2", procName, 1);
    }

    rp->index++;
    if (rp->mode != L_REG_COMPARE) return 0;

        /* Generate the golden file names */
    snprintf(namebuf, sizeof(namebuf), "%s_golden.%02d.", rp->testname, index1);
    sa = getSortedPathnamesInDirectory("/tmp/golden", namebuf, 0, 0);
    if (sarrayGetCount(sa) != 1) {
        sarrayDestroy(&sa);
        rp->success = FALSE;
        L_ERROR("golden file %s not found\n", procName, namebuf);
        return 1;
    }
    name1 = sarrayGetString(sa, 0, L_COPY);
    sarrayDestroy(&sa);

    snprintf(namebuf, sizeof(namebuf), "%s_golden.%02d.", rp->testname, index2);
    sa = getSortedPathnamesInDirectory("/tmp/golden", namebuf, 0, 0);
    if (sarrayGetCount(sa) != 1) {
        sarrayDestroy(&sa);
        rp->success = FALSE;
        FREE(name1);
        L_ERROR("golden file %s not found\n", procName, namebuf);
        return 1;
    }
    name2 = sarrayGetString(sa, 0, L_COPY);
    sarrayDestroy(&sa);

        /* Test and record on failure */
    filesAreIdentical(name1, name2, &same);
    if (!same) {
        fprintf(rp->fp,
                "Failure in %s_reg, index %d: comparing %s with %s\n",
                rp->testname, rp->index, name1, name2);
        fprintf(stderr,
                "Failure in %s_reg, index %d: comparing %s with %s\n",
                rp->testname, rp->index, name1, name2);
        rp->success = FALSE;
    }

    FREE(name1);
    FREE(name2);
    return 0;
}


/*!
 *  regTestWritePixAndCheck()
 *
 *      Input:  rp (regtest parameters)
 *              pix (to be written)
 *              format (of output pix)
 *      Return: 0 if OK, 1 on error (a failure in comparison is not an error)
 *
 *  Notes:
 *      (1) This function makes it easy to write the pix in a numbered
 *          sequence of files, and either to:
 *             (a) write the golden file ("generate" arg to regression test)
 *             (b) make a local file and "compare" with the golden file
 *             (c) make a local file and "display" the results
 *      (3) The canonical format of the local filename is:
 *            /tmp/regout/<root of main name>.<count>.<format extension string>
 *          e.g., for scale_reg,
 *            /tmp/regout/scale.0.png
 */
l_int32
regTestWritePixAndCheck(L_REGPARAMS  *rp,
                        PIX          *pix,
                        l_int32       format)
{
char   namebuf[256];

    PROCNAME("regTestWritePixAndCheck");

    if (!rp)
        return ERROR_INT("rp not defined", procName, 1);
    if (!pix) {
        rp->success = FALSE;
        return ERROR_INT("pix not defined", procName, 1);
    }
    if (format < 0 || format >= NumImageFileFormatExtensions) {
        rp->success = FALSE;
        return ERROR_INT("invalid format", procName, 1);
    }

        /* Generate the local file name */
    snprintf(namebuf, sizeof(namebuf), "/tmp/regout/%s.%02d.%s", rp->testname,
             rp->index + 1, ImageFileFormatExtensions[format]);

        /* Write the local file */
    if (pixGetDepth(pix) < 8)
        pixSetPadBits(pix, 0);
    pixWrite(namebuf, pix, format);

        /* Either write the golden file ("generate") or check the
           local file against an existing golden file ("compare") */
    regTestCheckFile(rp, namebuf);

    return 0;
}


/*!
 *  getRootNameFromArgv0()
 *
 *      Input:  argv0
 *      Return: root name (without the '_reg'), or null on error
 *
 *  Notes:
 *      (1) For example, from psioseg_reg, we want to extract
 *          just 'psioseg' as the root.
 *      (2) In unix with autotools, the executable is not X,
 *          but ./.libs/lt-X.   So in addition to stripping out the
 *          last 4 characters of the tail, we have to check for
 *          the '-' and strip out the "lt-" prefix if we find it.
 */
static char *
getRootNameFromArgv0(const char  *argv0)
{
l_int32  len;
char    *root;

    PROCNAME("getRootNameFromArgv0");

    splitPathAtDirectory(argv0, NULL, &root);
    if ((len = strlen(root)) <= 4) {
        FREE(root);
        return (char *)ERROR_PTR("invalid argv0; too small", procName, NULL);
    }

#ifndef _WIN32
    {
        char    *newroot;
        l_int32  loc;
        if (stringFindSubstr(root, "-", &loc)) {
            newroot = stringNew(root + loc + 1);  /* strip out "lt-" */
            FREE(root);
            root = newroot;
            len = strlen(root);
        }
    }
#else
    if (strstr(root, ".exe") != NULL)
        len -= 4;
#endif  /* ! _WIN32 */

    root[len - 4] = '\0';  /* remove the suffix */
    return root;
}

