
/*
 *@@sourcefile dosh2.c:
 *      dosh.c contains more Control Program helper functions.
 *
 *      This file is new with V0.9.4 (2000-07-26) [umoeller].
 *
 *      As opposed to the functions in dosh.c, these require
 *      linking against other helpers. As a result, these have
 *      been separated from dosh.c to allow linking against
 *      dosh.obj only.
 *
 *      Function prefixes:
 *      --  dosh*   Dos (Control Program) helper functions
 *
 *      This has the same header as dosh.c, dosh.h.
 *
 *      The partition functions in this file are based on
 *      code which has kindly been provided by Dmitry A. Steklenev.
 *      See doshGetPartitionsList for how to use these.
 *
 *      Note: Version numbering in this file relates to XWorkplace version
 *            numbering.
 *
 *@@header "helpers\dosh.h"
 *@@added V0.9.4 (2000-07-27) [umoeller]
 */

/*
 *      This file Copyright (C) 1997-2013 Ulrich M攍ler,
 *                                        Dmitry A. Steklenev.
 *      This file is part of the "XWorkplace helpers" source package.
 *      This 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 2 as it comes in the
 *      "COPYING" file of the XWorkplace main distribution.
 *      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.
 */

#define OS2EMX_PLAIN_CHAR
    // this is needed for "os2emx.h"; if this is defined,
    // emx will define PSZ as _signed_ char, otherwise
    // as unsigned char

#define INCL_DOSMODULEMGR
#define INCL_DOSPROCESS
#define INCL_DOSSESMGR
#define INCL_DOSQUEUES
#define INCL_DOSMISC
#define INCL_DOSDEVICES
#define INCL_DOSDEVIOCTL
#define INCL_DOSERRORS
#include <os2.h>

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>

#include "setup.h"                      // code generation and debugging options

#include "helpers\dosh.h"
#include "helpers\ensure.h"
#include "helpers\level.h"
#include "helpers\nls.h"
#include "helpers\standards.h"
#include "helpers\stringh.h"

#pragma hdrstop

/*
 *@@category: Helpers\Control program helpers\Miscellaneous
 */

/* ******************************************************************
 *
 *   Miscellaneous
 *
 ********************************************************************/

/*
 *@@ doshIsWarp4:
 *      checks the OS/2 system version number.
 *
 *      Returns:
 *
 *      -- 0 (FALSE): OS/2 2.x or Warp 3 is running.
 *
 *      -- 1: Warp 4.0 is running.
 *
 *      -- 2: Warp 4.5 kernel is running on Warp 4.0 (Warp 4 FP 13+).
 *
 *      -- 3: Warp 4.5 is running (WSeB or eCS or ACP/MCP), or even something newer.
 *
 *@@changed V0.9.2 (2000-03-05) [umoeller]: reported TRUE on Warp 3 also; fixed
 *@@changed V0.9.6 (2000-10-16) [umoeller]: patched for speed
 *@@changed V0.9.9 (2001-04-04) [umoeller]: now returning 2 for Warp 4.5 and above
 *@@changed V1.0.5 (2006-05-29) [pr]: now returning 3 for Warp 4.5 and above and 2 for
 *   Warp 4.0 FP13+; moved here from dosh.c
 */

ULONG doshIsWarp4(VOID)
{
    static BOOL     s_fQueried = FALSE;
    static ULONG    s_ulrc = 0;

    if (!s_fQueried)
    {
        // first call:
        ULONG       aulBuf[3];

        DosQuerySysInfo(QSV_VERSION_MAJOR,      // 11
                        QSV_VERSION_MINOR,      // 12
                        &aulBuf, sizeof(aulBuf));
        // Warp 3 is reported as 20.30
        // Warp 4 is reported as 20.40
        // Aurora is reported as 20.45 (regardless of convenience packs)

        if (aulBuf[0] > 20)        // major > 20; not the case with Warp 3, 4, 5
            s_ulrc = 3;
        else
            if (aulBuf[0] == 20)   // major == 20
                if (aulBuf[1] >= 45)   // minor >= 45  Warp 4 FP13 or later
                    s_ulrc = 2;
                else
                    if (aulBuf[1] == 40)   // minor == 40  Warp 4 pre-FP13
                        s_ulrc = 1;

        // Now check SYSLEVEL.OS2 to detect between Warp 4 and MCP
        if (s_ulrc == 2)
        {
            CHAR szName[CCHMAXPATH] = "?:\\OS2\\INSTALL\\SYSLEVEL.OS2";
            ULONG cbFile;
            PXFILE pFile;

            szName[0] = doshQueryBootDrive();
            if (!doshOpen(szName,
                          XOPEN_READ_EXISTING | XOPEN_BINARY,
                          &cbFile,
                          &pFile))
            {
                CHAR szVersion[2];
                ULONG ulSize;

                if (   !lvlQueryLevelFileData(pFile->hf,
                                              QLD_MINORVERSION,
                                              szVersion,
                                              sizeof(szVersion),
                                              &ulSize)
                    && (szVersion[0] >= '5'))   // minor >= 5  is MCP
                        s_ulrc = 3;

                doshClose(&pFile);
            }
        }

        s_fQueried = TRUE;
    }

    return (s_ulrc);
}

/*
 *@@ doshIsFixpak:
 *      checks if the OS/2 system is at a given fixpak level or higher
 *
 *      Returns:
 *
 *      -- FALSE: the system is at a lower level
 *
 *      -- TRUE: the system is at the exact or a higher level
 *
 *@@added V1.0.8 (2008-04-07) [chennecke]: @@fixes 1067
 */

BOOL doshIsFixpak(BOOL fIsMcp,          // in: MCP fixpak?
                  ULONG ulFixpakLevel)    // in: fixpak level number
{
    static BOOL     s_fQueried = FALSE;
    static BOOL     s_fRc = FALSE;

    if (!s_fQueried)
    {
        // first call:
        // check SYSLEVEL.OS2
        CHAR szName[CCHMAXPATH] = "?:\\OS2\\INSTALL\\SYSLEVEL.OS2";
        ULONG cbFile;
        PXFILE pFile;

        szName[0] = doshQueryBootDrive();
        if (!doshOpen(szName,
                      XOPEN_READ_EXISTING | XOPEN_BINARY,
                      &cbFile,
                      &pFile))
        {
            CHAR szCsdLevel[8];
            CHAR szCsdLevelNumber[4];
            ULONG ulSize;

            if (!lvlQueryLevelFileData(pFile->hf,
                                       QLD_CURRENTCSD,
                                       szCsdLevel,
                                       sizeof(szCsdLevel),
                                       &ulSize))
            {
                strncpy(szCsdLevelNumber, szCsdLevel + 4, 3);
                szCsdLevelNumber[sizeof(szCsdLevelNumber) - 1] = '\0';
                if (fIsMcp)
                {
                    if (   (szCsdLevel[3] == 'C')
                        && (atol(szCsdLevelNumber) >= ulFixpakLevel))
                        s_fRc = TRUE;
                }
                else
                {
                    if (   (szCsdLevel[3] == 'C')
                        || (    (szCsdLevel[3] == 'M')
                             && (atol(szCsdLevelNumber) >= ulFixpakLevel)))
                        s_fRc = TRUE;
                }
            }

            doshClose(&pFile);
        }

        s_fQueried = TRUE;
    }

    return (s_fRc);
}


/*
 *@@ doshIsValidFileName:
 *      this returns NO_ERROR only if pszFile is a valid file name.
 *      This may include a full path.
 *
 *      If a drive letter is specified, this checks for whether
 *      that drive is a FAT drive and adjust the checks accordingly,
 *      i.e. 8+3 syntax (per path component).
 *
 *      If no drive letter is specified, this check is performed
 *      for the current drive.
 *
 *      This also checks if pszFileNames contains characters which
 *      are invalid for the current drive.
 *
 *      Note: this performs syntactic checks only. This does not
 *      check for whether the specified path components exist.
 *      However, it _is_ checked for whether the given drive
 *      exists.
 *
 *      This func is especially useful to check filenames that
 *      have been entered by the user in a "Save as" dialog.
 *
 *      If an error is found, the corresponding DOS error code
 *      is returned:
 *      --  ERROR_INVALID_DRIVE
 *      --  ERROR_FILENAME_EXCED_RANGE  (on FAT: no 8+3 filename)
 *      --  ERROR_INVALID_NAME          (invalid character)
 *      --  ERROR_CURRENT_DIRECTORY     (if fFullyQualified: no full path specified)
 *
 *@@changed V0.9.2 (2000-03-11) [umoeller]: added fFullyQualified
 */

APIRET doshIsValidFileName(const char* pcszFile,
                           BOOL fFullyQualified)    // in: if TRUE, pcszFile must be fully q'fied
{
    APIRET  arc = NO_ERROR;
    CHAR    szPath[CCHMAXPATH+4] = " :";
    CHAR    szComponent[CCHMAXPATH];
    PSZ     p1, p2;
    BOOL    fIsFAT = FALSE;
    PSZ     pszInvalid;

    if (fFullyQualified)    // V0.9.2 (2000-03-11) [umoeller]
    {
        if (    (*(pcszFile + 1) != ':')
             || (*(pcszFile + 2) != '\\')
           )
            arc = ERROR_CURRENT_DIRECTORY;
    }

    // check drive first
    if (*(pcszFile + 1) == ':')
    {
        CHAR cDrive = toupper(*pcszFile);
        double d;
        // drive specified:
        strcpy(szPath, pcszFile);
        szPath[0] = toupper(*pcszFile);
        arc = doshQueryDiskFree(cDrive - 'A' + 1, &d);
    }
    else
    {
        // no drive specified: take current
        ULONG   ulDriveNum = 0,
                ulDriveMap = 0;
        arc = DosQueryCurrentDisk(&ulDriveNum, &ulDriveMap);
        szPath[0] = ((UCHAR)ulDriveNum) + 'A' - 1;
        szPath[1] = ':';
        strcpy(&szPath[2], pcszFile);
    }

    if (arc == NO_ERROR)
    {
        fIsFAT = doshIsFileOnFAT(szPath);

        pszInvalid = (fIsFAT)
                        ? "<>|+=:;,\"/[] "  // invalid characters in FAT
                        : "<>|:\"/";        // invalid characters in IFS's

        // now separate path components
        p1 = &szPath[2];       // advance past ':'

        do {

            if (*p1 == '\\')
                p1++;

            p2 = strchr(p1, '\\');
            if (p2 == NULL)
                p2 = p1 + strlen(p1);

            if (p1 != p2)
            {
                LONG    lDotOfs = -1,
                        lAfterDot = -1;
                ULONG   cbFile,
                        ul;
                PSZ     pSource = szComponent;

                strncpy(szComponent, p1, p2-p1);
                szComponent[p2-p1] = 0;
                cbFile = strlen(szComponent);

                // now check each path component
                for (ul = 0; ul < cbFile; ul++)
                {
                    if (fIsFAT)
                    {
                        // on FAT: only 8 characters allowed before dot
                        if (*pSource == '.')
                        {
                            lDotOfs = ul;
                            lAfterDot = 0;
                            if (ul > 7)
                                return ERROR_FILENAME_EXCED_RANGE;
                        }
                    }
                    // and check for invalid characters
                    if (strchr(pszInvalid, *pSource) != NULL)
                        return ERROR_INVALID_NAME;

                    pSource++;

                    // on FAT, allow only three chars after dot
                    if (fIsFAT)
                        if (lAfterDot != -1)
                        {
                            lAfterDot++;
                            if (lAfterDot > 3)
                                return ERROR_FILENAME_EXCED_RANGE;
                        }
                }

                // we are still missing the case of a FAT file
                // name without extension; if so, check whether
                // the file stem is <= 8 chars
                if (fIsFAT)
                    if (lDotOfs == -1)  // dot not found:
                        if (cbFile > 8)
                            return ERROR_FILENAME_EXCED_RANGE;
            }

            // go for next component
            p1 = p2+1;
        } while (*p2);
    }

    return arc;
}

/*
 *@@ doshMakeRealName:
 *      this copies pszSource to pszTarget, replacing
 *      all characters which are not supported by file
 *      systems with cReplace.
 *
 *      pszTarget must be at least the same size as pszSource.
 *      If (fIsFAT), the file name will be made FAT-compliant (8+3).
 *
 *      Returns TRUE if characters were replaced.
 *
 *@@changed V0.9.0 (99-11-06) [umoeller]: now replacing "*" too
 */

BOOL doshMakeRealName(PSZ pszTarget,    // out: new real name
                      PSZ pszSource,    // in: filename to translate
                      CHAR cReplace,    // in: replacement char for invalid
                                        //     characters (e.g. '!')
                      BOOL fIsFAT)      // in: make-FAT-compatible flag
{
    ULONG ul,
          cbSource = strlen(pszSource);
    LONG  lDotOfs = -1,
          lAfterDot = -1;
    BOOL  brc = FALSE;
    PSZ   pSource = pszSource,
          pTarget = pszTarget;

    const char *pcszInvalid = (fIsFAT)
                                   ? "*<>|+=:;,\"/\\[] "  // invalid characters in FAT
                                   : "*<>|:\"/\\"; // invalid characters in IFS's

    for (ul = 0; ul < cbSource; ul++)
    {
        if (fIsFAT)
        {
            // on FAT: truncate filename if neccessary
            if (*pSource == '.')
            {
                lDotOfs = ul;
                lAfterDot = 0;
                if (ul > 7) {
                    // only 8 characters allowed before dot,
                    // so set target ptr to dot pos
                    pTarget = pszTarget+8;
                }
            }
        }
        // and replace invalid characters
        if (strchr(pcszInvalid, *pSource) == NULL)
            *pTarget = *pSource;
        else
        {
            *pTarget = cReplace;
            brc = TRUE;
        }
        pTarget++;
        pSource++;

        // on FAT, allow only three chars after dot
        if (fIsFAT)
            if (lAfterDot != -1)
            {
                lAfterDot++;
                if (lAfterDot > 3)
                    break;
            }
    }
    *pTarget = '\0';

    if (fIsFAT)
    {
        // we are still missing the case of a FAT file
        // name without extension; if so, check whether
        // the file stem is <= 8 chars
        if (lDotOfs == -1)  // dot not found:
            if (cbSource > 8)
                *(pszTarget+8) = 0; // truncate

        // convert to upper case
        strupr(pszTarget);
    }

    return brc;
}

/*
 *@@ doshSetCurrentDir:
 *      sets the current working directory
 *      to the given path.
 *
 *      As opposed to DosSetCurrentDir, this
 *      one will change the current drive
 *      also, if one is specified.
 *
 *@@changed V0.9.9 (2001-04-04) [umoeller]: this returned an error even if none occurred, fixed
 */

APIRET doshSetCurrentDir(const char *pcszDir)
{
    APIRET  arc = NO_ERROR;

    if (    (!pcszDir)
         || (!(*pcszDir))
       )
        return ERROR_INVALID_PARAMETER;

    if (pcszDir[1] == ':')
    {
        // drive given:
        CHAR    cDrive = toupper(*(pcszDir));
        // change drive
        arc = DosSetDefaultDisk( (ULONG)(cDrive - 'A' + 1) );
                // 1 = A:, 2 = B:, ...
    }

    if (!arc)
        arc = DosSetCurrentDir((PSZ)pcszDir);

    return arc;       // V0.9.9 (2001-04-04) [umoeller]
}

/*
 *@@ CopyToBuffer:
 *      little helper for copying a string to
 *      a target buffer with length checking.
 *
 *      Returns:
 *
 *      --  NO_ERROR
 *
 *      --  ERROR_BUFFER_OVERFLOW if pszTarget does
 *          not have enough room to hold pcszSource
 *          (including the null terminator).
 *
 *@@added V0.9.16 (2001-10-08) [umoeller]
 */

STATIC APIRET CopyToBuffer(PSZ pszTarget,      // out: target buffer
                           PCSZ pcszSource,    // in: source string
                           ULONG cbTarget)     // in: size of target buffer
{
    ULONG ulLength = strlen(pcszSource);
    if (ulLength < cbTarget)
    {
        memcpy(pszTarget,
               pcszSource,
               ulLength + 1);
        return NO_ERROR;
    }

    return ERROR_BUFFER_OVERFLOW;
}

/*
 *@@ doshSearchPath:
 *      replacement for DosSearchPath.
 *
 *      This looks along all directories which are
 *      specified in the value of the given environment
 *      variable if pcszFile is found.
 *
 *      As opposed to the stupid DosSearchPath, this
 *      ignores subdirectories in the path particles.
 *      For example, DosSearchPath would usually not
 *      find an INSTALL file because \OS2 contains
 *      an INSTALL directory, or NETSCAPE because
 *      \OS2\INSTALL contains a NETSCAPE directory.
 *
 *      Returns:
 *
 *      --  NO_ERROR: pszExecutable has received the
 *          full path of pcszFile.
 *
 *      --  ERROR_FILE_NOT_FOUND: pcszFile was not found
 *          in the specified path (or is a directory).
 *
 *      --  ERROR_BUFFER_OVERFLOW: pcszFile was found, but
 *          the pszExecutable buffer is too small to hold
 *          the full path.
 *
 *@@added V0.9.16 (2001-10-08) [umoeller]
 *@@changed V1.0.0 (2002-11-23) [umoeller]: allowing NULL pcszPath to search for "PATH"
 */

APIRET doshSearchPath(const char *pcszPath,     // in: path variable name; if NULL, we use "PATH"
                      const char *pcszFile,     // in: file to look for (e.g. "LVM.EXE")
                      PSZ pszExecutable,        // out: full path (e.g. "F:\os2\lvm.exe")
                      ULONG cbExecutable)       // in: sizeof (*pszExecutable)
{
    APIRET arc = NO_ERROR;

    // get the PATH value
    PCSZ pcszPathValue;

    if (!pcszPath)
        pcszPath = "PATH";      // V1.0.0 (2002-11-23) [umoeller]

    if (!(arc = DosScanEnv((PSZ)pcszPath,
#if __cplusplus
                           &pcszPathValue)))
#else
                           (PSZ*)&pcszPathValue)))
#endif
    {
        // run thru the path components
        PSZ pszPathCopy;
        if (pszPathCopy = strdup(pcszPathValue))
        {
            PSZ pszToken = strtok(pszPathCopy, ";");
            while (pszToken)
            {
                CHAR szFileMask[2*CCHMAXPATH];
                FILESTATUS3 fs3;

                sprintf(szFileMask,
                        "%s\\%s",
                        pszToken,           // path particle
                        pcszFile);          // e.g. "netscape"

                if (    (!(arc = DosQueryPathInfo(szFileMask,
                                                  FIL_STANDARD,
                                                  &fs3,
                                                  sizeof(fs3))))
                     // make sure it's not a directory
                     // and that it's not hidden
                     && (!(fs3.attrFile & (FILE_DIRECTORY | FILE_HIDDEN)))
                   )
                {
                    // copy
                    arc = CopyToBuffer(pszExecutable,
                                       szFileMask,
                                       cbExecutable);
                    // and stop
                    break;
                }
                else
                    arc = ERROR_FILE_NOT_FOUND;
                    // and search on

                pszToken = strtok(NULL, ";");
            };

            free(pszPathCopy);
        }
        else
            arc = ERROR_NOT_ENOUGH_MEMORY;
    }

    return arc;
}

/*
 *@@ doshSearchDirs:
 *      This looks along all directories which are
 *      specified in the passed directory list
 *      if pcszFile is found.
 *
 *      As opposed to the stupid DosSearchPath, this
 *      ignores subdirectories in the path particles.
 *      For example, DosSearchPath would usually not
 *      find an INSTALL file because \OS2 contains
 *      an INSTALL directory, or NETSCAPE because
 *      \OS2\INSTALL contains a NETSCAPE directory.
 *
 *      Returns:
 *
 *      --  NO_ERROR: pszExecutable has received the
 *          full path of pcszFile.
 *
 *      --  ERROR_FILE_NOT_FOUND: pcszFile was not found
 *          in the specified path (or is a directory).
 *
 *      --  ERROR_BUFFER_OVERFLOW: pcszFile was found, but
 *          the pszExecutable buffer is too small to hold
 *          the full path.
 *
 *@@added V1.0.4 (2005-06-16) [chennecke]: blatantly stolen from doshSearchPath
 */

APIRET doshSearchDirs(const char *pcszDirList,  // in: list of directories in PATH style
                      const char *pcszFile,     // in: file to look for (e.g. "LVM.EXE")
                      PSZ pszExecutable,        // out: full path (e.g. "F:\os2\lvm.exe")
                      ULONG cbExecutable)       // in: sizeof (*pszExecutable)
{
    APIRET arc = NO_ERROR;

    // run thru the path components
    PSZ pszPathCopy;
    if (pszPathCopy = strdup(pcszDirList))
    {
        PSZ pszToken = strtok(pszPathCopy, ";");
        while (pszToken)
        {
            CHAR szFileMask[2*CCHMAXPATH];
            FILESTATUS3 fs3;

            sprintf(szFileMask,
                    "%s\\%s",
                    pszToken,           // path particle
                    pcszFile);          // e.g. "netscape"

            if (    (!(arc = DosQueryPathInfo(szFileMask,
                                              FIL_STANDARD,
                                              &fs3,
                                              sizeof(fs3))))
                 // make sure it's not a directory
                 // and that it's not hidden
                 && (!(fs3.attrFile & (FILE_DIRECTORY | FILE_HIDDEN)))
               )
            {
                // copy
                arc = CopyToBuffer(pszExecutable,
                                   szFileMask,
                                   cbExecutable);
                // and stop
                break;
            }
            else
                arc = ERROR_FILE_NOT_FOUND;
                // and search on

            pszToken = strtok(NULL, ";");
        };

        free(pszPathCopy);
    }
    else
        arc = ERROR_NOT_ENOUGH_MEMORY;

    return arc;
}

/*
 * FindFile:
 *      helper for doshFindExecutable.
 *
 *added V0.9.11 (2001-04-25) [umoeller]
 *@@changed V0.9.16 (2001-10-08) [umoeller]: rewrote second half for DosSearchPath replacement, which returns directories too
 */

STATIC APIRET FindFile(const char *pcszCommand,      // in: command (e.g. "lvm")
                       PSZ pszExecutable,            // out: full path (e.g. "F:\os2\lvm.exe")
                       ULONG cbExecutable)           // in: sizeof (*pszExecutable)
{
    APIRET arc = NO_ERROR;
    FILESTATUS3 fs3;

    if (    (strchr(pcszCommand, '\\'))
         || (strchr(pcszCommand, ':'))
       )
    {
        // looks like this is qualified:
        arc = DosQueryPathInfo((PSZ)pcszCommand,
                               FIL_STANDARD,
                               &fs3,
                               sizeof(fs3));
        if (!arc)
            if (!(fs3.attrFile & FILE_DIRECTORY))
                arc = CopyToBuffer(pszExecutable,
                                   pcszCommand,
                                   cbExecutable);
            else
                // directory:
                arc = ERROR_INVALID_EXE_SIGNATURE;
    }
    else
    {
        // non-qualified:
        /* arc = DosSearchPath(SEARCH_IGNORENETERRS
                                | SEARCH_ENVIRONMENT
                                | SEARCH_CUR_DIRECTORY,
                            "PATH",
                            (PSZ)pcszCommand,
                            pszExecutable,
                            cbExecutable); */
            // The above is not useable. It returns directories
            // on the path... for example, it returns \OS2\INSTALL\NETSCAPE
            // if netscape is looked for. So we search manually... sigh.
            // V0.9.16 (2001-10-08) [umoeller]
        arc = doshSearchPath("PATH",
                             pcszCommand,
                             pszExecutable,
                             cbExecutable);
    }

    return arc;
}

/*
 *@@ doshFindExecutable:
 *      this attempts to find an executable by doing the
 *      following:
 *
 *      1)  If pcszCommand appears to be qualified (i.e. contains
 *          a backslash), this checks for whether the file exists.
 *          If it is a directory, ERROR_INVALID_EXE_SIGNATURE is
 *          returned.
 *
 *      2)  If pcszCommand contains no backslash, this searches
 *          all directories on the PATH in order to find the full
 *          path of the executable. Starting with V0.9.16, we
 *          use doshSearchPath for that.
 *
 *      papcszExtensions determines if additional searches are to be
 *      performed if the file doesn't exist (case 1) or doshSearchPath
 *      returned ERROR_FILE_NOT_FOUND (case 2).
 *      This must point to an array of strings specifying the extra
 *      extensions to search for.
 *
 *      If both papcszExtensions and cExtensions are null, no
 *      extra searches are performed.
 *
 *      Returns:
 *
 *      --  NO_ERROR: pszExecutable has received the full path of
 *          the executable found by DosSearchPath.
 *
 *      --  ERROR_FILE_NOT_FOUND
 *
 *      --  ERROR_BUFFER_OVERFLOW: pcszCommand was found, but
 *          the pszExecutable buffer is too small to hold
 *          the full path.
 *
 *      Example:
 *
 +      const char *aExtensions[] = {  "EXE",
 +                                     "COM",
 +                                     "CMD"
 +                                  };
 +      CHAR szExecutable[CCHMAXPATH];
 +      APIRET arc = doshFindExecutable("lvm",
 +                                      szExecutable,
 +                                      sizeof(szExecutable),
 +                                      aExtensions,
 +                                      3);
 *
 *@@added V0.9.9 (2001-03-07) [umoeller]
 *@@changed V0.9.11 (2001-04-25) [umoeller]: this never worked for qualified pcszCommand's, fixed
 */

APIRET doshFindExecutable(const char *pcszCommand,      // in: command (e.g. "lvm")
                          PSZ pszExecutable,            // out: full path (e.g. "F:\os2\lvm.exe")
                          ULONG cbExecutable,           // in: sizeof (*pszExecutable)
                          const char **papcszExtensions, // in: array of extensions (without dots)
                          ULONG cExtensions)            // in: array item count
{
    APIRET arc = FindFile(pcszCommand,
                          pszExecutable,
                          cbExecutable);

    if (    (arc == ERROR_FILE_NOT_FOUND)           // not found?
         && (cExtensions)                    // any extra searches wanted?
       )
    {
        // try additional things then
        PSZ psz2;
        if (psz2 = (PSZ)malloc(strlen(pcszCommand) + 20))
        {
            ULONG   ul;
            for (ul = 0;
                 ul < cExtensions;
                 ul++)
            {
                const char *pcszExtThis = papcszExtensions[ul];
                sprintf(psz2,
                        "%s.%s",
                        pcszCommand,
                        pcszExtThis);
                arc = FindFile(psz2,
                               pszExecutable,
                               cbExecutable);
                if (arc != ERROR_FILE_NOT_FOUND)
                    break;
            }

            free(psz2);
        }
        else
            arc = ERROR_NOT_ENOUGH_MEMORY;
    }

    return arc;
}

/*
 *@@category: Helpers\Control program helpers\Partitions info
 *      functions for retrieving partition information directly
 *      from the partition tables on the disk. See doshGetPartitionsList.
 */

/********************************************************************
 *
 *   Partition functions
 *
 ********************************************************************/

/*
 *@@ doshQueryDiskCount:
 *      returns the no. of physical disks installed
 *      on the system.
 *
 *@@added V0.9.0 [umoeller]
 */

UINT doshQueryDiskCount(VOID)
{
    USHORT usCount = 0;
    DosPhysicalDisk(INFO_COUNT_PARTITIONABLE_DISKS, &usCount, 2, 0, 0);
    return usCount;
}

/*
 *@@ doshType2FSName:
 *      this returns a static, zero-terminated string
 *      for the given FS type, or NULL if the type
 *      is unknown.
 *
 *@@added V0.9.0 [umoeller]
 *@@changed V0.9.16 (2001-10-08) [umoeller]: rewritten
 */

const char* doshType2FSName(unsigned char bFSType)  // in: FS type
{
    switch (bFSType)
    {
        case 0x00: return "empty";
        case 0x01: return "DOS 12-bit FAT < 10 Mb";
        case 0x02: return "XENIX root file system";
        case 0x03: return "XENIX /usr file system (obsolete)";
        case 0x04: return "DOS 16-bit FAT < 32 Mb";
        case 0x05: return "DOS 3.3+ extended partition";
        case 0x06: return "DOS 3.31+ 16-bit FAT > 32 Mb";
        case 0x07: return "HPFS/NTFS/QNX/Advanced Unix";
        case 0x08: return "OS/2 1.0-1.3/AIX/Commodore/DELL";
        case 0x09: return "AIX data/Coherent";
        case 0x0A: return "OS/2 Boot Manager/OPUS/Coherent Swap";
        case 0x0B: return "Windows95 with 32-bit FAT";
        case 0x0C: return "Windows95 with 32-bit FAT (LBA)";
        case 0x0E: return "Windows 95 VFAT (06h plus LBA)";
        case 0x0F: return "Windows 95 VFAT (05h plus LBA)";
        case 0x10: return "OPUS";
        case 0x11: return "OS/2 Boot Manager hidden 12-bit FAT";
        case 0x12: return "Compaq Diagnostics";
        case 0x14: return "OS/2 Boot Manager hidden sub-32M 16-bit FAT";
        case 0x16: return "OS/2 Boot Manager hidden over-32M 16-bit FAT";
        case 0x17: return "OS/2 Boot Manager hidden HPFS";
        case 0x18: return "AST special Windows swap file (\"Zero-Volt Suspend\")";
        // case 0x21: reserved
        // case 0x23: reserved
        case 0x24: return "NEC MS-DOS 3.x";
        // case 0x26: reserved
        // case 0x31: reserved
        // case 0x33: reserved
        // case 0x34: reserved
        case 0x35: return "JFS";
        // case 0x36: reserved
        case 0x38: return "Theos";
        case 0x3C: return "PowerQuest PartitionMagic recovery partition";
        case 0x40: return "VENIX 80286";
        case 0x41: return "Personal RISC Boot";
        case 0x42: return "SFS (Secure File System) by Peter Gutmann";
        case 0x50: return "OnTrack Disk Manager, read-only";
        case 0x51: return "OnTrack Disk Manager, read/write";
        case 0x52: return "CP/M or Microport System V/386";
        case 0x53: return "OnTrack Disk Manager, write-only???";
        case 0x54: return "OnTrack Disk Manager (DDO)";
        case 0x56: return "GoldenBow VFeature";
        case 0x61: return "SpeedStor";
        case 0x63: return "Unix SysV/386, 386/ix or Mach, MtXinu BSD 4.3 on Mach or GNU HURD";
        case 0x64: return "Novell NetWare 286";
        case 0x65: return "Novell NetWare (3.11)";
        case 0x67:
        case 0x68:
        case 0x69: return "Novell";
        case 0x70: return "DiskSecure Multi-Boot";
        // case 0x71: reserved
        // case 0x73: reserved
        // case 0x74: reserved
        case 0x75: return "PC/IX";
        // case 0x76: reserved
        case 0x80: return "Minix v1.1 - 1.4a";
        case 0x81: return "Minix v1.4b+ or Linux or Mitac Advanced Disk Manager";
        case 0x82: return "Linux Swap or Prime";
        case 0x83: return "Linux native file system (ext2fs/xiafs)";
        case 0x84: return "OS/2-renumbered type 04h (hidden DOS C: drive)";
        case 0x86: return "FAT16 volume/stripe set (Windows NT)";
        case 0x87: return "HPFS Fault-Tolerant mirrored partition or NTFS volume/stripe set";
        case 0x93: return "Amoeba file system";
        case 0x94: return "Amoeba bad block table";
        case 0xA0: return "Phoenix NoteBIOS Power Management \"Save-to-Disk\" partition";
        // case 0xA1: reserved
        // case 0xA3: reserved
        // case 0xA4: reserved
        case 0xA5: return "FreeBSD, BSD/386";
        // case 0xA6: reserved
        // case 0xB1: reserved
        // case 0xB3: reserved
        // case 0xB4: reserved
        // case 0xB6: reserved
        case 0xB7: return "BSDI file system (secondarily swap)";
        case 0xB8: return "BSDI swap (secondarily file system)";
        case 0xC1: return "DR DOS 6.0 LOGIN.EXE-secured 12-bit FAT";
        case 0xC4: return "DR DOS 6.0 LOGIN.EXE-secured 16-bit FAT";
        case 0xC6: return "DR DOS 6.0 LOGIN.EXE-secured Huge partition or NT corrupted FAT16 volume/stripe set";
        case 0xC7: return "Syrinx Boot or corrupted NTFS volume/stripe set";
        case 0xD8: return "CP/M-86";
        case 0xDB: return "CP/M, Concurrent CP/M, Concurrent DOS, Convergent Technologies OS";
        case 0xE1: return "SpeedStor 12-bit FAT extended partition";
        case 0xE3: return "DOS read-only or Storage Dimensions";
        case 0xE4: return "SpeedStor 16-bit FAT extended partition";
        // case 0xE5: reserved
        // case 0xE6: reserved
        case 0xF1: return "Storage Dimensions";
        case 0xF2: return "DOS 3.3+ secondary partition";
        // case 0xF3: reserved
        case 0xF4: return "SpeedStor or Storage Dimensions";
        // case 0xF6: reserved
        case 0xFE: return "LANstep or IBM PS/2 IML";
        case 0xFF: return "Xenix bad block table";
    }

    return NULL;
}

/*
 * AppendPartition:
 *      this appends the given partition information to
 *      the given partition list. To do this, a new
 *      PARTITIONINFO structure is created and appended
 *      in a list (managed thru the PARTITIONINFO.pNext
 *      items).
 *
 *      pppiThis must be a pointer to a pointer to a PARTITIONINFO.
 *      With each call of this function, this pointer is advanced
 *      to point to the newly created PARTITIONINFO, so before
 *      calling this function for the first time,
 *
 *@@added V0.9.0 [umoeller]
 *@@changed V0.9.20 (2002-08-10) [umoeller]: fixed truncated LVM names
 */

STATIC APIRET AppendPartition(PARTITIONINFO **pppiFirst,
                              PARTITIONINFO **pppiThis,    // in/out: partition info; pointer will be advanced
                              PUSHORT posCount,            // in/out: partition count
                              BYTE bDisk,                  // in: disk of partition
                              const char *pszBootName,     // in: boot partition name
                              CHAR cLetter,                // in/out: drive letter
                              BYTE bFsType,                // in: file system type
                              BOOL fPrimary,               // in: primary?
                              BOOL fBootable,
                              ULONG ulSectors)             // in: no. of sectors
{
    APIRET arc = NO_ERROR;
    PPARTITIONINFO ppiNew;
    if (ppiNew = NEW(PARTITIONINFO))
    {
        ZERO(ppiNew);

        // store data
        ppiNew->bDisk = bDisk;
        if (    (fBootable)
             && (pszBootName)
           )
        {
            // fixed truncated LVM names V0.9.20 (2002-08-10) [umoeller]
            strhncpy0(ppiNew->szBootName,
                      pszBootName,
                      sizeof(ppiNew->szBootName));
        }
        else
            ppiNew->szBootName[0] = 0;
        ppiNew->cLetter = cLetter;
        ppiNew->bFSType = bFsType;
        ppiNew->pcszFSType = doshType2FSName(bFsType);
        ppiNew->fPrimary = fPrimary;
        ppiNew->fBootable = fBootable;
        ppiNew->ulSize = ulSectors / 2048;

        ppiNew->pNext = NULL;

        (*posCount)++;

        if (*pppiFirst == (PPARTITIONINFO)NULL)
        {
            // first call:
            *pppiFirst = ppiNew;
            *pppiThis = ppiNew;
        }
        else
        {
            // append to list
            (**pppiThis).pNext = ppiNew;
            *pppiThis = ppiNew;
        }
    }
    else
        arc = ERROR_NOT_ENOUGH_MEMORY;

    return arc;
}

#ifndef __XWPLITE__

/*
 *@@ doshReadSector:
 *      reads a physical disk sector.
 *
 *      If NO_ERROR is returned, the sector contents
 *      have been stored in *buff.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 *@@changed V0.9.9 (2001-04-04) [umoeller]: added more error checking
 */

APIRET doshReadSector(USHORT disk,      // in: physical disk no. (1, 2, 3, ...)
                      void *buff,
                      USHORT head,
                      USHORT cylinder,
                      USHORT sector)
{
    APIRET  arc;
    HFILE   dh = 0;
    char    dn[256];

    sprintf(dn, "%u:", disk);
    if (!(arc = DosPhysicalDisk(INFO_GETIOCTLHANDLE, &dh, 2, dn, 3)))
    {
        TRACKLAYOUT DiskIOParm;
        ULONG IOCtlDataLength = sizeof(DiskIOParm);
        ULONG IOCtlParmLength = 512;

        DiskIOParm.bCommand = 0;
        DiskIOParm.usHead = head;
        DiskIOParm.usCylinder = cylinder;
        DiskIOParm.usFirstSector = 0;
        DiskIOParm.cSectors = 1;
        DiskIOParm.TrackTable[0].usSectorNumber = sector;
        DiskIOParm.TrackTable[0].usSectorSize = 512;

        arc = DosDevIOCtl(dh,
                          IOCTL_PHYSICALDISK, PDSK_READPHYSTRACK,
                          &DiskIOParm, IOCtlParmLength, &IOCtlParmLength,
                          buff       , IOCtlDataLength, &IOCtlDataLength);

        DosPhysicalDisk(INFO_FREEIOCTLHANDLE, 0, 0, &dh, 2);
    }

    return arc;
}

// Sector and Cylinder values are actually 6 bits and 10 bits:
//
//   1 1 1 1 1 1
//  󵕚򪹀򪑦򬯎򬇴򫠚򪹀�1履�
//  砪 c c c c c c c C c S s s s s s�
//  滥聊聊聊聊聊聊聊聊聊聊聊聊聊聊聊�
//
// The high two bits of the second byte are used as the high bits
// of a 10-bit value.  This allows for as many as 1024 cylinders
// and 64 sectors per cylinder.

/*
 * GetCyl:
 *      get cylinder number.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 */

STATIC USHORT GetCyl(USHORT rBeginSecCyl)
{
    return   ((rBeginSecCyl & 0x00C0) << 2)
           + ((rBeginSecCyl & 0xFF00) >> 8);
}

/*
 * GetSec:
 *      get sector number.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 */

STATIC USHORT GetSec(USHORT rBeginSecCyl)
{
    return rBeginSecCyl & 0x003F;
}

/*
 *@@ doshGetBootManager:
 *      this goes thru the master boot records on all
 *      disks to find the boot manager partition.
 *
 *      Returns:
 *
 *      -- NO_ERROR: boot manager found; in that case,
 *                   information about the boot manager
 *                   is written into *pusDisk, *pusPart,
 *                   *BmInfo. Any of these pointers can
 *                   be NULL if you're not interested.
 *
 *      -- ERROR_NOT_SUPPORTED (50): boot manager not installed.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 */

APIRET doshGetBootManager(USHORT   *pusDisk,    // out: if != NULL, boot manager disk (1, 2, ...)
                          USHORT   *pusPart,    // out: if != NULL, index of bmgr primary partition (0-3)
                          PAR_INFO *pBmInfo)    // out: if != NULL, boot manager partition info
{
    APIRET          arc = NO_ERROR;
    USHORT          count = doshQueryDiskCount();    // Physical disk number
    MBR_INFO        MBoot;      // Master Boot
    USHORT          usDisk;

    if (count > 8)              // Not above 8 disks
        count = 8;

    for (usDisk = 1; usDisk <= count; usDisk++)
    {
        USHORT usPrim = 0;

        // for each disk, read the MBR, which has the
        // primary partitions
        if ((arc = doshReadSector(usDisk,
                                  &MBoot,
                                  0,            // head
                                  0,            // cylinder
                                  1)))          // sector
            return arc;

        // scan primary partitions for whether
        // BootManager partition exists
        for (usPrim = 0; usPrim < 4; usPrim++)
        {
            if (MBoot.sPrtnInfo[usPrim].bFileSysCode == 0x0A)
            {
                // this is boot manager:
                if (pBmInfo)
                    *pBmInfo = MBoot.sPrtnInfo[usPrim];
                if (pusPart)
                    *pusPart = usPrim;
                if (pusDisk)
                    *pusDisk = usDisk;
                // stop scanning
                return NO_ERROR;
            }
        }
    }

    return ERROR_NOT_SUPPORTED;
}

/*
 * GetPrimaryPartitions:
 *      this returns the primary partitions.
 *
 *      This gets called from doshGetPartitionsList.
 *
 *      Returns:
 *
 *      -- ERROR_INVALID_PARAMETER: BMInfo is NULL.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 */

STATIC APIRET GetPrimaryPartitions(PARTITIONINFO **pppiFirst,
                                   PARTITIONINFO **pppiThis,
                                   PUSHORT posCount,       // in/out: partition count
                                   PCHAR pcLetter,         // in/out: drive letter counter
                                   UINT BmDisk,            // in: physical disk (1, 2, 3, ...) of boot manager or null
                                   PAR_INFO* pBmInfo,      // in: info returned by doshGetBootManager or NULL
                                   UINT iDisk)             // in: system's physical disk count
{
    APIRET  arc = NO_ERROR;

    if (!pBmInfo)
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        SYS_INFO        MName[32];  // Name Space from Boot Manager
        memset(&MName, 0, sizeof(MName));

        // read boot manager name table;
        // this is in the boot manager primary partition
        // at sector offset 3 (?!?)
        if (!(arc = doshReadSector(BmDisk,
                                   &MName,
                                   // head, cylinder, sector of bmgr primary partition:
                                   pBmInfo->bBeginHead,
                                   GetCyl(pBmInfo->rBeginSecCyl),
                                   GetSec(pBmInfo->rBeginSecCyl) + 3)))
        {
            // got bmgr name table:
            MBR_INFO        MBoot;      // Master Boot
            USHORT          i;

            // read master boot record of this disk
            if (!(arc = doshReadSector(iDisk,
                                       &MBoot,
                                       0,           // head
                                       0,           // cylinder
                                       1)))         // sector
            {
                for (i = 0;
                     i < 4;     // there can be only four primary partitions
                     i++)
                {
                    // skip unused partition, BootManager or Extended partition
                    if (    (MBoot.sPrtnInfo[i].bFileSysCode)  // skip unused
                        &&  (MBoot.sPrtnInfo[i].bFileSysCode != 0x0A) // skip boot manager
                        &&  (MBoot.sPrtnInfo[i].bFileSysCode != 0x05) // skip extended partition
                       )
                    {
                        BOOL fBootable = (    (pBmInfo)
                                           && (MName[(iDisk-1) * 4 + i].bootable & 0x01)
                                         );
                        // store this partition
                        if ((arc = AppendPartition(pppiFirst,
                                                   pppiThis,
                                                   posCount,
                                                   iDisk,
                                                   (fBootable)
                                                     ? (char*)&MName[(iDisk - 1) * 4 + i].name
                                                     : "",
                                                   *pcLetter,
                                                   MBoot.sPrtnInfo[i].bFileSysCode,
                                                   TRUE,        // primary
                                                   fBootable,
                                                   MBoot.sPrtnInfo[i].lTotalSects)))
                            return arc;
                    }
                }
            }
        }
    }

    return arc;
}

/*
 * GetLogicalDrives:
 *      this returns info for the logical drives
 *      in the extended partition. This gets called
 *      from GetExtendedPartition.
 *
 *      This gets called from GetExtendedPartition.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 */

STATIC APIRET GetLogicalDrives(PARTITIONINFO **pppiFirst,
                               PARTITIONINFO **pppiThis,
                               PUSHORT posCount,
                               PCHAR pcLetter,
                               PAR_INFO* PrInfo,                    // in: MBR entry of extended partition
                               UINT PrDisk,
                               PAR_INFO* BmInfo)
{
    APIRET          arc = NO_ERROR;
    EXT_INFO        MBoot;      // Master Boot
    USHORT          i;

    if ((arc = doshReadSector(PrDisk,
                              &MBoot,
                              PrInfo->bBeginHead,
                              GetCyl(PrInfo->rBeginSecCyl),
                              GetSec(PrInfo->rBeginSecCyl))))
        return arc;

    for (i = 0; i < 4; i++)
    {
        // skip unused partition or BootManager partition
        if (    (MBoot.sPrtnInfo[i].bFileSysCode)
             && (MBoot.sPrtnInfo[i].bFileSysCode != 0x0A)
           )
        {
            BOOL    fBootable = FALSE;
            BOOL    fAssignLetter = FALSE;

            // special work around extended partition
            if (MBoot.sPrtnInfo[i].bFileSysCode == 0x05)
            {
                if ((arc = GetLogicalDrives(pppiFirst,
                                            pppiThis,
                                            posCount,
                                            pcLetter,
                                            &MBoot.sPrtnInfo[i],
                                            PrDisk,
                                            BmInfo)))
                    return arc;

                continue;
            }

            // raise driver letter if OS/2 would recognize this drive
            if (    (MBoot.sPrtnInfo[i].bFileSysCode < 0x75)
               )
                fAssignLetter = TRUE;

            if (fAssignLetter)
                (*pcLetter)++;

            fBootable = (   (BmInfo)
                         && ((MBoot.sBmNames[i].bootable & 0x01) != 0)
                        );

            if ((arc = AppendPartition(pppiFirst,
                                       pppiThis,
                                       posCount,
                                       PrDisk,
                                       (fBootable)
                                         ? (char*)&MBoot.sBmNames[i].name
                                         : "",
                                       (fAssignLetter)
                                         ? *pcLetter
                                         : ' ',
                                       MBoot.sPrtnInfo[i].bFileSysCode,
                                       FALSE,        // primary
                                       fBootable,    // bootable
                                       MBoot.sPrtnInfo[i].lTotalSects)))
                return arc;
        }
    }

    return NO_ERROR;
}

/*
 * GetExtendedPartition:
 *      this finds the extended partition on the given
 *      drive and calls GetLogicalDrives in turn.
 *
 *      This gets called from doshGetPartitionsList.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 */

STATIC APIRET GetExtendedPartition(PARTITIONINFO **pppiFirst,
                                   PARTITIONINFO **pppiThis,
                                   PUSHORT posCount,
                                   PCHAR pcLetter,
                                   PAR_INFO* BmInfo,
                                   UINT iDisk)                // in: disk to query
{
    APIRET          arc = NO_ERROR;
    MBR_INFO        MBoot;      // Master Boot
    USHORT          i;

    if ((arc = doshReadSector(iDisk, &MBoot, 0, 0, 1)))
        return arc;

    // go thru MBR entries to find extended partition
    for (i = 0;
         i < 4;
         i++)
    {
        if (MBoot.sPrtnInfo[i].bFileSysCode == 0x05)
        {
            if ((arc = GetLogicalDrives(pppiFirst,
                                        pppiThis,
                                        posCount,
                                        pcLetter,
                                        &MBoot.sPrtnInfo[i],
                                        iDisk,
                                        BmInfo)))
                return arc;
        }
    }

    return NO_ERROR;
}

/*
 *@@ ReadFDiskPartitions:
 *      helper for doshGetPartitionsList for non-LVM
 *      systems.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.16 (2001-10-08) [umoeller]
 */

STATIC APIRET ReadFDiskPartitions(PARTITIONINFO **ppPartitionInfos,
                                  USHORT *pcPartitions,
                                  PUSHORT pusContext)              // out: error context
{
    APIRET          arc = NO_ERROR;

    PAR_INFO        BmInfo;     // BootManager partition
    USHORT          usBmDisk;     // BootManager disk
    USHORT          cDisks = doshQueryDiskCount();    // physical disks count
    USHORT          i;

    CHAR            cLetter = 'C';  // first drive letter

    PARTITIONINFO   *ppiTemp = NULL;

    if (cDisks > 8)              // Not above 8 disks
        cDisks = 8;

    // get boot manager disk and info
    if ((arc = doshGetBootManager(&usBmDisk,
                                  NULL,
                                  &BmInfo)) != NO_ERROR)
    {
        *pusContext = 1;
    }
    else
    {
        // on each disk, read primary partitions
        for (i = 1; i <= cDisks; i++)
        {
            if ((arc = GetPrimaryPartitions(ppPartitionInfos,
                                            &ppiTemp,
                                            pcPartitions,
                                            &cLetter,
                                            usBmDisk,
                                            usBmDisk ? &BmInfo : 0,
                                            i)))
            {
                *pusContext = 2;
            }
        }

        if (!arc && usBmDisk)
        {
            // boot manager found:
            // on each disk, read extended partition
            // with logical drives
            for (i = 1; i <= cDisks; i++)
            {
                if ((arc = GetExtendedPartition(ppPartitionInfos,
                                                &ppiTemp,
                                                pcPartitions,
                                                &cLetter,
                                                &BmInfo,
                                                i)))
                {
                    *pusContext = 3;
                }
            }
        }
    } // end else if ((arc = doshGetBootManager(&usBmDisk,

    return arc;
}

#endif

/*
 *@@ CleanPartitionInfos:
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

STATIC VOID CleanPartitionInfos(PPARTITIONINFO ppiThis)
{
    while (ppiThis)
    {
        PPARTITIONINFO ppiNext = ppiThis->pNext;
        free(ppiThis);
        ppiThis = ppiNext;
    }
}

/*
 *@@ doshGetPartitionsList:
 *      this returns lots of information about the
 *      partitions on all physical disks, which is
 *      read directly from the MBRs and partition
 *      tables.
 *
 *      If NO_ERROR is returned by this function,
 *      *ppPartitionInfo points to a linked list of
 *      PARTITIONINFO structures, which has
 *      *pusPartitionCount items.
 *
 *      In that case, use doshFreePartitionsList to
 *      free the resources allocated by this function.
 *
 *      What this function returns depends on whether
 *      LVM is installed.
 *
 *      --  If LVM.DLL is found on the LIBPATH, this opens
 *          the LVM engine and returns the info from the
 *          LVM engine in the PARTITIONINFO structures.
 *          The partitions are then sorted by disk in
 *          ascending order.
 *
 *      --  Otherwise, we parse the partition tables
 *          manually. The linked list then starts out with
 *          all the primary partitions, followed by the
 *          logical drives in the extended partitions.
 *          This function attempts to guess the correct drive
 *          letters and stores these with the PARTITIONINFO
 *          items, but there's no guarantee that this is
 *          correct. We correctly ignore Linux partitions here
 *          and give all primary partitions the C: letter, but
 *          I have no idea what happens with NTFS partitions,
 *          since I have none.
 *
 *      If an error != NO_ERROR is returned, *pusContext
 *      will be set to one of the following:
 *
 *      --  1: boot manager not found
 *
 *      --  2: primary partitions error
 *
 *      --  3: secondary partitions error
 *
 *      --  0: something else.
 *
 *      Originally contributed by Dmitry A. Steklenev.
 *
 *@@added V0.9.0 [umoeller]
 *@@changed V0.9.9 (2001-04-07) [umoeller]: added transparent LVM support; changed prototype
 *@@changed V0.9.9 (2001-04-07) [umoeller]: fixed memory leaks on errors
 */

APIRET doshGetPartitionsList(PPARTITIONSLIST *ppList,
                             PUSHORT pusContext)                // out: error context
{
    APIRET          arc = NO_ERROR;

    PLVMINFO        pLVMInfo = NULL;

    PARTITIONINFO   *pPartitionInfos = NULL; // linked list of all partitions
    USHORT          cPartitions = 0;        // bootable partition count

    if (!ppList)
        return ERROR_INVALID_PARAMETER;

    if (!(arc = doshQueryLVMInfo(&pLVMInfo)))
    {
        // LVM installed:
        arc = doshReadLVMPartitions(pLVMInfo,         // in: LVM info
                                    &pPartitionInfos, // out: partitions array
                                    &cPartitions);      // out: partitions count
        // copied to output below

        if (arc)
        {
            // error: start over
            doshFreeLVMInfo(pLVMInfo);
            CleanPartitionInfos(pPartitionInfos);
            pPartitionInfos = NULL;
            cPartitions = 0;
        }
    }

#ifndef __XWPLITE__
    if (arc)
        // LVM not installed, or failed:
        // parse partitions manually
        arc = ReadFDiskPartitions(&pPartitionInfos,
                                  &cPartitions,
                                  pusContext);
#endif

    if (!arc)
    {
        // no error so far:
        *pusContext = 0;

        *ppList = NEW(PARTITIONSLIST);
        if (!(*ppList))
            arc = ERROR_NOT_ENOUGH_MEMORY;
        else
        {
            ZERO(*ppList);

            (*ppList)->pPartitionInfo = pPartitionInfos;
            (*ppList)->cPartitions = cPartitions;

            _Pmpf((__FUNCTION__ ": returning %d partitions", cPartitions));
        }
    }

    if (arc)
        CleanPartitionInfos(pPartitionInfos);

    _Pmpf((__FUNCTION__ ": exiting, arc = %d", arc));

    return arc;
}

/*
 *@@ doshFreePartitionsList:
 *      this frees the resources allocated by
 *      doshGetPartitionsList.
 *
 *@@added V0.9.0 [umoeller]
 */

APIRET doshFreePartitionsList(PPARTITIONSLIST ppList)
{
    if (!ppList)
        return ERROR_INVALID_PARAMETER;

    CleanPartitionInfos(ppList->pPartitionInfo);
    doshFreeLVMInfo(ppList->pLVMInfo);
    free(ppList);

    return NO_ERROR;
}

/********************************************************************
 *
 *   LVM declarations
 *
 ********************************************************************/

/*
 *@@category: Helpers\Control program helpers\Partitions info\Quick LVM Interface
 *      functions for transparently interfacing LVM.DLL.
 */

typedef unsigned char       BOOLEAN;
typedef unsigned short int  CARDINAL16;
typedef unsigned long       CARDINAL32;
typedef unsigned int        CARDINAL;
typedef unsigned long       DoubleWord;

#ifdef ADDRESS
#undef ADDRESS
#endif

typedef void* ADDRESS;

#pragma pack(1)

#define DISK_NAME_SIZE          20
#define FILESYSTEM_NAME_SIZE    20
#define PARTITION_NAME_SIZE     20
#define VOLUME_NAME_SIZE        20

/*
 *@@ Drive_Control_Record:
 *      invariant for a disk drive.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _Drive_Control_Record
{
    CARDINAL32   Drive_Number;                      // OS/2 Drive Number for this drive.
    CARDINAL32   Drive_Size;                        // The total number of sectors on the drive.
    DoubleWord   Drive_Serial_Number;               // The serial number assigned to this drive.  For info. purposes only.
    ADDRESS      Drive_Handle;                      // Handle used for operations on the disk that this record corresponds to.
    CARDINAL32   Cylinder_Count;                    // The number of cylinders on the drive.
    CARDINAL32   Heads_Per_Cylinder;                // The number of heads per cylinder for this drive.
    CARDINAL32   Sectors_Per_Track;                 // The number of sectors per track for this drive.
    BOOLEAN      Drive_Is_PRM;                      // Set to TRUE if this drive is a PRM.
    BYTE         Reserved[3];                       // Alignment.
} Drive_Control_Record;

/*
 *@@ Drive_Control_Array:
 *      returned by the Get_Drive_Control_Data function
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _Drive_Control_Array
{
    Drive_Control_Record *   Drive_Control_Data;    // An array of drive control records.
    CARDINAL32               Count;                 // The number of entries in the array of drive control records.
} Drive_Control_Array;

/*
 *@@ Drive_Information_Record:
 *      defines the information that can be changed for a specific disk drive.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _Drive_Information_Record
{
    CARDINAL32   Total_Available_Sectors;           // The number of sectors on the disk which are not currently assigned to a partition.
    CARDINAL32   Largest_Free_Block_Of_Sectors;     // The number of sectors in the largest contiguous block of available sectors.
    BOOLEAN      Corrupt_Partition_Table;           // If TRUE, then the partitioning information found on the drive is incorrect!
    BOOLEAN      Unusable;                          // If TRUE, the drive's MBR is not accessible and the drive can not be partitioned.
    BOOLEAN      IO_Error;                          // If TRUE, then the last I/O operation on this drive failed!
    BOOLEAN      Is_Big_Floppy;                     // If TRUE, then the drive is a PRM formatted as a big floppy (i.e. the old style removable media support).
    char         Drive_Name[DISK_NAME_SIZE];        // User assigned name for this disk drive.
} Drive_Information_Record;

/*
 *@@ Partition_Information_Record:
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _Partition_Information_Record
{
    ADDRESS      Partition_Handle;
            // The handle used to perform operations on this partition.
    ADDRESS      Volume_Handle;
            // If this partition is part of a volume, this will be the handle of
            // the volume.  If this partition is NOT part of a volume, then this
            // handle will be 0.
    ADDRESS      Drive_Handle;
            // The handle for the drive this partition resides on.
    DoubleWord   Partition_Serial_Number;
            // The serial number assigned to this partition.
    CARDINAL32   Partition_Start;
            // The LBA of the first sector of the partition.
    CARDINAL32   True_Partition_Size;
            // The total number of sectors comprising the partition.
    CARDINAL32   Usable_Partition_Size;
            // The size of the partition as reported to the IFSM.  This is the
            // size of the partition less any LVM overhead.
    CARDINAL32   Boot_Limit;
            // The maximum number of sectors from this block of free space that
            // can be used to create a bootable partition if you allocate from the
            // beginning of the block of free space.
    BOOLEAN      Spanned_Volume;
            // TRUE if this partition is part of a multi-partition volume.
    BOOLEAN      Primary_Partition;
            // True or False.  Any non-zero value here indicates that this partition
            // is a primary partition.  Zero here indicates that this partition is
            // a "logical drive" - i.e. it resides inside of an extended partition.
    BYTE         Active_Flag;
            // 80 = Partition is marked as being active.
            // 0 = Partition is not active.
    BYTE         OS_Flag;
            // This field is from the partition table.  It is known as the OS flag,
            // the Partition Type Field, Filesystem Type, and various other names.
            //      Values of interest
            //      If this field is: (values are in hex)
            //      07 = The partition is a compatibility partition formatted for use
            //      with an installable filesystem, such as HPFS or JFS.
            //      00 = Unformatted partition
            //      01 = FAT12 filesystem is in use on this partition.
            //      04 = FAT16 filesystem is in use on this partition.
            //      0A = OS/2 Boot Manager Partition
            //      35 = LVM partition
            //      84 = OS/2 FAT16 partition which has been relabeled by Boot Manager to "Hide" it.
    BYTE         Partition_Type;
            // 0 = Free Space
            // 1 = LVM Partition (Part of an LVM Volume.)
            // 2 = Compatibility Partition
            // All other values are reserved for future use.
    BYTE         Partition_Status;
            // 0 = Free Space
            // 1 = In Use - i.e. already assigned to a volume.
            // 2 = Available - i.e. not currently assigned to a volume.
    BOOLEAN      On_Boot_Manager_Menu;
            // Set to TRUE if this partition is not part of a Volume yet is on the
            // Boot Manager Menu.
    BYTE         Reserved;
            // Alignment.
    char         Volume_Drive_Letter;
            // The drive letter assigned to the volume that this partition is a part of.
    char         Drive_Name[DISK_NAME_SIZE];
            // User assigned name for this disk drive.
    char         File_System_Name[FILESYSTEM_NAME_SIZE];
            // The name of the filesystem in use on this partition, if it is known.
    char         Partition_Name[PARTITION_NAME_SIZE];
            // The user assigned name for this partition.
    char         Volume_Name[VOLUME_NAME_SIZE];
            // If this partition is part of a volume, then this will be the
            // name of the volume that this partition is a part of.  If this
            // record represents free space, then the Volume_Name will be
            // "FREE SPACE xx", where xx is a unique numeric ID generated by
            // LVM.DLL.  Otherwise it will be an empty string.
} Partition_Information_Record;

// The following defines are for use with the Partition_Type field in the
// Partition_Information_Record.
#define FREE_SPACE_PARTITION     0
#define LVM_PARTITION            1
#define COMPATIBILITY_PARTITION  2

// The following defines are for use with the Partition_Status field in the
// Partition_Information_Record.
#define PARTITION_IS_IN_USE      1
#define PARTITION_IS_AVAILABLE   2
#define PARTITION_IS_FREE_SPACE  0

/*
 *@@ Partition_Information_Array:
 *      returned by various functions in the LVM Engine.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _Partition_Information_Array
{
    Partition_Information_Record * Partition_Array; // An array of Partition_Information_Records.
    CARDINAL32                     Count;           // The number of entries in the Partition_Array.
} Partition_Information_Array;

/*
 *@@ Volume_Information_Record:
 *      variable information for a volume.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _Volume_Information_Record
{
    CARDINAL32 Volume_Size;
            // The number of sectors comprising the volume.
    CARDINAL32 Partition_Count;
            // The number of partitions which comprise this volume.
    CARDINAL32 Drive_Letter_Conflict;
            // 0   indicates that the drive letter preference for this volume is unique.
            // 1   indicates that the drive letter preference for this volume
            //       is not unique, but this volume got its preferred drive letter anyway.
            // 2   indicates that the drive letter preference for this volume
            //       is not unique, and this volume did NOT get its preferred drive letter.
            // 4   indicates that this volume is currently "hidden" - i.e. it has
            //       no drive letter preference at the current time.
    BOOLEAN    Compatibility_Volume;
            // TRUE if this is for a compatibility volume, FALSE otherwise.
    BOOLEAN    Bootable;
            // Set to TRUE if this volume appears on the Boot Manager menu, or if it is
            // a compatibility volume and its corresponding partition is the first active
            // primary partition on the first drive.
    char       Drive_Letter_Preference;
            // The drive letter that this volume desires to be.
    char       Current_Drive_Letter;
            // The drive letter currently used to access this volume.
            // May be different than Drive_Letter_Preference if there was a conflict ( i.e. Drive_Letter_Preference
            // is already in use by another volume ).
    char       Initial_Drive_Letter;
            // The drive letter assigned to this volume by the operating system
            // when LVM was started. This may be different from the
            // Drive_Letter_Preference if there were conflicts, and
            // may be different from the Current_Drive_Letter.  This
            // will be 0x0 if the Volume did not exist when the LVM Engine
            // was opened (i.e. it was created during this LVM session).
    BOOLEAN    New_Volume;
            // Set to FALSE if this volume existed before the LVM Engine was
            // opened.  Set to TRUE if this volume was created after the LVM
            // Engine was opened.
    BYTE       Status;
            // 0 = None.
            // 1 = Bootable
            // 2 = Startable
            // 3 = Installable.
    BYTE       Reserved_1;
    char       Volume_Name[VOLUME_NAME_SIZE];
            // The user assigned name for this volume.
    char       File_System_Name[FILESYSTEM_NAME_SIZE];
            // The name of the filesystem in use on this partition, if it
            // is known.
} Volume_Information_Record;

#pragma pack()

/********************************************************************
 *
 *   Quick LVM Interface API
 *
 ********************************************************************/

/*
 *@@ LVMINFOPRIVATE:
 *      private structure used by doshQueryLVMInfo.
 *      This is what the LVMINFO pointer really
 *      points to.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

typedef struct _LVMINFOPRIVATE
{
    LVMINFO             LVMInfo;            // public structure (dosh.h)

    // function pointers resolved from LVM.DLL

    void (* _System     Open_LVM_Engine)(BOOLEAN Ignore_CHS,
                                         CARDINAL32 *Error_Code);

    void (* _System     Free_Engine_Memory)(ADDRESS Object);

    void (* _System     Close_LVM_Engine)(void);

    Drive_Control_Array (* _System
                        Get_Drive_Control_Data)(CARDINAL32 *Error_Code);

    Drive_Information_Record (* _System
                        Get_Drive_Status)(ADDRESS Drive_Handle,
                                          CARDINAL32 *Error_Code);

    Partition_Information_Array (* _System
                        Get_Partitions)(ADDRESS Handle,
                                        CARDINAL32 *Error_Code);

    Volume_Information_Record (*_System
                        Get_Volume_Information)(ADDRESS Volume_Handle,
                                                CARDINAL32 *Error_Code);

} LVMINFOPRIVATE, *PLVMINFOPRIVATE;

#define LVM_ERROR_FIRST             20000

/*
 *@@ doshQueryLVMInfo:
 *      creates an LVMINFO structure if LVM is installed.
 *      Returns that structure (which the caller must free
 *      using doshFreeLVMInfo) or NULL if LVM.DLL was not
 *      found along the LIBPATH.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

APIRET doshQueryLVMInfo(PLVMINFO *ppLVMInfo)
{
    APIRET          arc = NO_ERROR;
    CHAR            szError[100];
    PLVMINFOPRIVATE pLVMInfo = NULL;
    HMODULE         hmodLVM = NULLHANDLE;

    if (!(arc = DosLoadModule(szError,
                              sizeof(szError),
                              "LVM",
                              &hmodLVM)))
    {
        // got LVM.DLL:
        pLVMInfo = NEW(LVMINFOPRIVATE);
        if (!pLVMInfo)
            arc = ERROR_NOT_ENOUGH_MEMORY;
        else
        {
            // array of function pointers to be resolved from LVM.DLL
            RESOLVEFUNCTION     aFunctions[] =
                    {
                        "Open_LVM_Engine", (PFN*)&pLVMInfo->Open_LVM_Engine,
                        "Free_Engine_Memory", (PFN*)&pLVMInfo->Free_Engine_Memory,
                        "Close_LVM_Engine", (PFN*)&pLVMInfo->Close_LVM_Engine,
                        "Get_Drive_Control_Data", (PFN*)&pLVMInfo->Get_Drive_Control_Data,
                        "Get_Drive_Status", (PFN*)&pLVMInfo->Get_Drive_Status,
                        "Get_Partitions", (PFN*)&pLVMInfo->Get_Partitions,
                        "Get_Volume_Information", (PFN*)&pLVMInfo->Get_Volume_Information
                    };
            ULONG               ul;

            ZERO(pLVMInfo);

            pLVMInfo->LVMInfo.hmodLVM = hmodLVM;

            // now resolve function pointers
            for (ul = 0;
                 ul < ARRAYITEMCOUNT(aFunctions);
                 ul++)
            {
                PRESOLVEFUNCTION pFuncThis = &aFunctions[ul];
                arc = DosQueryProcAddr(hmodLVM,
                                       0,               // ordinal, ignored
                                       (PSZ)pFuncThis->pcszFunctionName,
                                       pFuncThis->ppFuncAddress);
                if (!pFuncThis->ppFuncAddress)
                    arc = ERROR_INVALID_NAME;

                if (arc)
                    break;
            }
        }
    }

    if (arc)
        doshFreeLVMInfo((PLVMINFO)pLVMInfo);
    else
        *ppLVMInfo = (PLVMINFO)pLVMInfo;

    return arc;
}

/*
 *@@ doshReadLVMPartitions:
 *      using the LVMINFO parameter from doshQueryLVMInfo,
 *      builds an array of PARTITIONINFO structures with
 *      the data returned from LVM.DLL.
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

APIRET doshReadLVMPartitions(PLVMINFO pInfo,         // in: LVM info
                             PPARTITIONINFO *ppPartitionInfo, // out: partitions array
                             PUSHORT pcPartitions)      // out: partitions count
{
    APIRET          arc = NO_ERROR;
    CARDINAL32      Error = 0;

    PARTITIONINFO   *pPartitionInfos = NULL, // linked list of all partitions
                    *ppiTemp = NULL;
    USHORT          cPartitions = 0;        // bootable partition count
    PLVMINFOPRIVATE pLVMInfo = (PLVMINFOPRIVATE)pInfo;

    _Pmpf((__FUNCTION__ ": entering"));

    if (!pLVMInfo)
        return ERROR_INVALID_PARAMETER;

    // initialize LVM engine
    pLVMInfo->Open_LVM_Engine(TRUE,
                              &Error);

    _Pmpf(("  Open_LVM_Engine Error: %d", Error));

    if (!Error)
    {
        Drive_Control_Array DCA = pLVMInfo->Get_Drive_Control_Data(&Error);
                        // member records to be freed

        _Pmpf(("  Get_Drive_Control_Data Error: %d, drive count: %d", Error, DCA.Count));

        if (    (!Error)
             && (DCA.Count)
           )
        {
            // DCA.Drive_Control_Data now contains drive information records;
            // this must be freed
            ULONG   ulDisk;

            for (ulDisk = 0;
                 ulDisk < DCA.Count;
                 ulDisk++)
            {
                Drive_Control_Record *pDriveControlRecord
                    = &DCA.Drive_Control_Data[ulDisk];
                ADDRESS hDrive = pDriveControlRecord->Drive_Handle;

                /* Drive_Information_Record pDriveInfoRecord
                    = pLVMInfo->Get_Drive_Status(hDrive,
                                                 &Error);

                _Pmpf(("  drive %d Get_Drive_Status Error: %d", ulDisk, Error));

                if (!Error) */
                {
                    Partition_Information_Array PIA
                        = pLVMInfo->Get_Partitions(hDrive,
                                                   &Error);

                    _Pmpf(("    Get_Partitions Error: %d", Error));

                    if (!Error)
                    {
                        // PIA.Partition_Array now contains
                        // Partition_Information_Record; must be freed

                        // now go thru partitions of this drive
                        ULONG ulPart;
                        for (ulPart = 0;
                             ulPart < PIA.Count;
                             ulPart++)
                        {
                            Partition_Information_Record *pPartition
                                = &PIA.Partition_Array[ulPart];
                            Volume_Information_Record VolumeInfo;

                            const char  *pcszBootName = NULL; // for now
                            BOOL        fBootable = FALSE;

                            if (pPartition->Volume_Handle)
                            {
                                // this partition is part of a volume:
                                // only then can it be bootable...
                                // get the volume info
                                VolumeInfo
                                    = pLVMInfo->Get_Volume_Information(pPartition->Volume_Handle,
                                                                       &Error);
                                pcszBootName = VolumeInfo.Volume_Name;

                                fBootable = (VolumeInfo.Status == 1);
                            }


                            if (arc = AppendPartition(&pPartitionInfos,
                                                      &ppiTemp,
                                                      &cPartitions,
                                                      ulDisk + 1,
                                                      pcszBootName,
                                                      pPartition->Volume_Drive_Letter,
                                                      pPartition->OS_Flag,  // FS type
                                                      pPartition->Primary_Partition,
                                                      fBootable,
                                                      pPartition->True_Partition_Size))
                                break;
                        }

                        // clean up partitions
                        pLVMInfo->Free_Engine_Memory(PIA.Partition_Array);
                    }
                }
                /* else
                    // error:
                    break; */
            }

            // clean up drive data
            pLVMInfo->Free_Engine_Memory(DCA.Drive_Control_Data);
        }
    }

    // close LVM
    pLVMInfo->Close_LVM_Engine();

    if (Error)
    {
        // if we got an error, return it with the
        // LVM error offset
        arc = LVM_ERROR_FIRST + Error;

        CleanPartitionInfos(pPartitionInfos);
    }

    if (!arc)
    {
        *ppPartitionInfo = pPartitionInfos;
        *pcPartitions = cPartitions;
    }

    _Pmpf((__FUNCTION__ ": exiting, arg = %d", arc));

    return arc;
}

/*
 *@@ doshFreeLVMInfo:
 *
 *@@added V0.9.9 (2001-04-07) [umoeller]
 */

VOID doshFreeLVMInfo(PLVMINFO pInfo)
{
    if (pInfo)
    {
        if (pInfo->hmodLVM)
            DosFreeModule(pInfo->hmodLVM);

        free(pInfo);
    }
}

/*
 *@@category: Helpers\Control program helpers\Wildcard matching
 *      See doshMatch.
 */

/* ******************************************************************
 *
 *   Wildcard matching
 *
 ********************************************************************/

/*
 * PerformMatch:
 *      compares a single path component. The input strings must
 *      not have slashes or backslashes in them.
 *
 *      fHasDot must be true if pName contains at least one dot.
 *
 *      Note that this function is recursive.
 */

STATIC BOOL PerformMatch(PCSZ pMask,
                         PCSZ pName,
                         int fHasDot)
{
    while (TRUE)
    {
        // go thru the pMask char by char
        switch (*pMask)
        {
            case 0:
                // if we've reached the end of the mask,
                // we better have the end of the name too
                if (*pName == 0)
                    return TRUE;
                return FALSE;

            case '?':
                // a question mark matches one single character;
                // it does _not_ match a dot;
                // at the end of the component, it also matches
                // no characters
                if (    (*pName != '.')
                     && (*pName != 0)
                   )
                    ++pName;
                ++pMask;
            break;

            case '*':
                // asterisk matches zero or more characters

                // skip extra asterisks
                /*
                do
                {
                    ++pMask;
                } while (*pMask == '*');
                */

                while (*(++pMask) == '*')       // V0.9.20 (2002-07-25) [umoeller]
                    ;

                // pMask points to after '*';
                // pName is unchanged... so for each pName
                // that follows, check if it matches
                while (TRUE)
                {
                    if (PerformMatch(pMask, pName, fHasDot))
                        // the remainder matched:
                        // then everything matches
                        return TRUE;

                    if (*pName == 0)
                        return FALSE;

                    // didn't match: try next pName
                    ++pName;
                }

            case '.':
                // a dot matches a dot only, even if the name doesn't
                // have one at the end
                ++pMask;
                if (*pName == '.')
                    ++pName;
                else if (    (fHasDot)
                          || (*pName != 0)
                        )
                    return FALSE;
            break;

            default:
                if (*pMask++ != *pName++)
                    return FALSE;
            break;
        }
    }
}

/*
 *@@ doshMatchCase:
 *      this matches '*' and '?' wildcards, similar to what
 *      DosEditName does. However, this does not require a
 *      file to be present, but works on strings only.
 *
 *      Returns TRUE if the given name matches the given mask.
 *
 *      This accepts both short and fully qualified masks and
 *      names, but the following rules apply:
 *
 *      --  Either both the mask and the name must be fully
 *          qualified, or both must not. Otherwise the match fails.
 *
 *      --  If fully qualified, only the last component may contain
 *          wildcards.
 *
 *      --  This compares WITH respect to case always. Upper-case
 *          both the mask and the name before calling this, or
 *          use doshMatch instead.
 *
 *      --  As opposed to the WPS, this handles multiple dots in
 *          filenames correctly. For example, the WPS will not
 *          match "*.ZIP" against "whatever-0.9.3.zip", but this
 *          one will.
 *
 *      This replaces strhMatchOS2 which has been removed with
 *      V0.9.16 and is a lot faster than the old code, which has
 *      been completely rewritten.
 *
 *@@added V0.9.16 (2002-01-01) [umoeller]
 */

BOOL doshMatchCase(const char *pcszMask,     // in: mask (e.g. "*.TXT")
                   const char *pcszName)     // in: string to check (e.g. "TEST.TXT")
{
    BOOL    brc = FALSE;

    PCSZ    pLastMaskComponent,
            pLastNameComponent;

    ULONG   cbMaskPath = 0,
            cbNamePath = 0;

    if (pLastMaskComponent = strrchr(pcszMask, '\\'))
    {
        // length of path component
        cbMaskPath = pLastMaskComponent - pcszMask;
        pLastMaskComponent++;
    }
    else
        pLastMaskComponent = pcszMask;

    if (pLastNameComponent = strrchr(pcszName, '\\'))
    {
        // length of path component
        cbNamePath = pLastNameComponent - pcszName;
        pLastNameComponent++;
    }
    else
        pLastNameComponent = pcszName;

    // compare paths; if the lengths are different
    // or memcmp fails, we can't match
    if (    (cbMaskPath == cbNamePath)      // can both be null
         && (    (cbMaskPath == 0)
              || (!memcmp(pcszMask, pcszName, cbMaskPath))
            )
       )
    {
        // alright, paths match:
        brc = PerformMatch(pLastMaskComponent,
                           pLastNameComponent,
                           // has dot?
                           (strchr(pLastNameComponent, '.') != NULL));

    }

    return brc;
}

/*
 *@@ doshMatchCaseNoPath:
 *      like doshMatchCase, but is faster if you are sure that
 *      neither pcszMask nor pcszName contain path separators
 *      ("\" characters). In other words, this is for short
 *      filenames.
 *
 *@@added V0.9.20 (2002-07-25) [umoeller]
 */

BOOL doshMatchCaseNoPath(const char *pcszMask,     // in: mask (e.g. "*.TXT")
                         const char *pcszName)     // in: string to check (e.g. "TEST.TXT")
{
    return PerformMatch(pcszMask,
                        pcszName,
                        // has dot?
                        (strchr(pcszName, '.') != NULL));
}

/*
 *@@ doshMatch:
 *      like doshMatchCase, but compares without respect
 *      to case.
 *
 *@@added V0.9.16 (2002-01-26) [umoeller]
 */

BOOL doshMatch(const char *pcszMask,     // in: mask (e.g. "*.TXT")
               const char *pcszName)     // in: string to check (e.g. "TEST.TXT")
{
    ULONG   cbMask = strlen(pcszMask),
            cbName = strlen(pcszName);
    PSZ     pszMask = (PSZ)_alloca(cbMask + 1),
            pszName = (PSZ)_alloca(cbName + 1);

    memcpy(pszMask, pcszMask, cbMask + 1);
    nlsUpper(pszMask);
    memcpy(pszName, pcszName, cbName + 1);
    nlsUpper(pszName);

    return doshMatchCase(pszMask,
                         pszName);
}
