
/*
 *@@sourcefile dosh.c:
 *      dosh.c contains Control Program helper functions.
 *
 *      This file has miscellaneous system functions,
 *      drive helpers, file helpers, and partition functions.
 *
 *      Usage: All OS/2 programs.
 *
 *      Function prefixes (new with V0.81):
 *      --  dosh*   Dos (Control Program) helper functions
 *
 *      These funcs are forward-declared in dosh.h, which
 *      must be #include'd first.
 *
 *      The resulting dosh.obj object file can be linked
 *      against any application object file. As opposed to
 *      the code in dosh2.c, it does not require any other
 *      code from the helpers.
 *
 *      dosh.obj can also be used with the VAC subsystem
 *      library (/rn compiler option).
 *
 *      Note: Version numbering in this file relates to XWorkplace version
 *            numbering.
 *
 *@@header "helpers\dosh.h"
 */

/*
 *      This file Copyright (C) 1997-2012 Ulrich Mller.
 *      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_DOSEXCEPTIONS
#define INCL_DOSSESMGR
#define INCL_DOSQUEUES
#define INCL_DOSSEMAPHORES
#define INCL_DOSMISC
#define INCL_DOSDEVICES
#define INCL_DOSDEVIOCTL
#define INCL_DOSERRORS

#define INCL_KBD
#include <os2.h>

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <ctype.h>

#include "setup.h"                      // code generation and debugging options

#include "helpers\dosh.h"
#include "helpers\standards.h"

#pragma hdrstop

// static const CHAR  G_acDriveLetters[28] = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/*
 *@@category: Helpers\Control program helpers\Wrappers
 */

/* ******************************************************************
 *
 *   Wrappers
 *
 ********************************************************************/

#ifdef DOSH_STANDARDWRAPPERS

    /*
     *@@ doshSleep:
     *
     *@@added V0.9.16 (2002-01-26) [umoeller]
     */

    APIRET doshSleep(ULONG msec)
    {
        // put the call in brackets so the macro won't apply here
        return (DosSleep)(msec);
    }

    /*
     *@@ doshCreateMutexSem:
     *
     *@@added V0.9.16 (2002-01-26) [umoeller]
     */

    APIRET doshCreateMutexSem(PSZ pszName,
                              PHMTX phmtx,
                              ULONG flAttr,
                              BOOL32 fState)
    {
        // put the call in brackets so the macro won't apply here
        return (DosCreateMutexSem)(pszName, phmtx, flAttr, fState);
    }

    /*
     *@@ doshRequestMutexSem:
     *
     *@@added V0.9.16 (2002-01-26) [umoeller]
     */

    APIRET doshRequestMutexSem(HMTX hmtx, ULONG ulTimeout)
    {
        return (DosRequestMutexSem)(hmtx, ulTimeout);
    }

    /*
     *@@ doshReleaseMutexSem:
     *
     *@@added V0.9.16 (2002-01-26) [umoeller]
     */

    APIRET doshReleaseMutexSem(HMTX hmtx)
    {
        return (DosReleaseMutexSem)(hmtx);
    }

    /*
     *@@ doshSetExceptionHandler:
     *
     *@@added V0.9.16 (2002-01-26) [umoeller]
     */

    APIRET doshSetExceptionHandler(PEXCEPTIONREGISTRATIONRECORD pERegRec)
    {
        // put the call in brackets so the macro won't apply here
        return (DosSetExceptionHandler)(pERegRec);
    }

    /*
     *@@ doshUnsetExceptionHandler:
     *
     *@@added V0.9.16 (2002-01-26) [umoeller]
     */

    APIRET doshUnsetExceptionHandler(PEXCEPTIONREGISTRATIONRECORD pERegRec)
    {
        // put the call in brackets so the macro won't apply here
        return (DosUnsetExceptionHandler)(pERegRec);
    }

#endif

/*
 *@@category: Helpers\Control program helpers\Miscellaneous
 *      Miscellaneous helpers in dosh.c that didn't fit into any other
 *      category.
 */

/* ******************************************************************
 *
 *   Miscellaneous
 *
 ********************************************************************/

/*
 *@@ doshGetChar:
 *      reads a single character from the keyboard.
 *      Useful for VIO sessions, since there's great
 *      confusion between the various C dialects about
 *      how to use getc(), and getc() doesn't work
 *      with the VAC subsystem library.
 *
 *@@added V0.9.4 (2000-07-27) [umoeller]
 */

CHAR doshGetChar(VOID)
{
    // CHAR    c;
    // ULONG   ulRead = 0;

    KBDKEYINFO kki;
    KbdCharIn(&kki,
              0, // wait
              0);

    return kki.chChar;
}

/*
 *@@ doshQueryShiftState:
 *      returns TRUE if any of the SHIFT keys are
 *      currently pressed. Useful for checks during
 *      WM_COMMAND messages from menus.
 *
 *@@changed V0.9.5 (2000-09-27) [umoeller]: added error checking
 */

BOOL doshQueryShiftState(VOID)
{
    BOOL            brc = FALSE;
    APIRET          arc = NO_ERROR;
    HFILE           hfKbd;
    ULONG           ulAction;

    if (!(arc = DosOpen("KBD$", &hfKbd, &ulAction, 0,
                        FILE_NORMAL,
                        FILE_OPEN,
                        OPEN_ACCESS_READONLY | OPEN_SHARE_DENYWRITE,
                        (PEAOP2)NULL)))
    {
        SHIFTSTATE      ShiftState;
        ULONG           cbDataLen = sizeof(ShiftState);

        if (!(arc = DosDevIOCtl(hfKbd, IOCTL_KEYBOARD, KBD_GETSHIFTSTATE,
                                NULL, 0, NULL,      // no parameters
                                &ShiftState, cbDataLen, &cbDataLen)))
            brc = ((ShiftState.fsState & 3) != 0);

        DosClose(hfKbd);
    }

    return brc;
}

/*
 *@@ doshQuerySysErrorMsg:
 *      this retrieves the error message for a system error
 *      (APIRET) from the system error message file (OSO001.MSG).
 *      This file better be on the DPATH (it normally is).
 *
 *      This returns the string in the "SYSxxx: blahblah" style,
 *      which is normally displayed on the command line when
 *      errors occur.
 *
 *      The error message is returned in a newly allocated
 *      buffer, which should be free()'d afterwards.
 *
 *      The returned string is properly null-terminated but
 *      should not end with a line break (\r or \n) if I see
 *      this correctly.
 *
 *      Returns NULL upon errors.
 *
 *@@changed V1.0.0 (2002-09-24) [umoeller]: now supporting NET messages as well
 */

PSZ doshQuerySysErrorMsg(APIRET arc)    // in: DOS error code
{
    PSZ     pszReturn = NULL;
    CHAR    szDosError[1000];
    ULONG   cbDosError = 0;
    PCSZ    pcszMsgFile;

    if (    (arc >= 2100)
         && (arc <= 8000)
       )
        pcszMsgFile = "NET.MSG";
    else
        pcszMsgFile = "OSO001.MSG";        // default OS/2 message file

    if (!DosGetMessage(NULL, 0,       // no string replacements
                       szDosError, sizeof(szDosError),
                       arc,
                       (PSZ)pcszMsgFile,
                       &cbDosError))
    {
        szDosError[cbDosError - 2] = 0;
        pszReturn = strdup(szDosError);
    }

    return pszReturn;
}

/*
 *@@ doshQuerySysUptime:
 *      returns the system uptime in milliseconds.
 *      This can be used for time comparisons.
 *
 *@@added V0.9.12 (2001-05-18) [umoeller]
 */

ULONG doshQuerySysUptime(VOID)
{
    ULONG ulms;
    DosQuerySysInfo(QSV_MS_COUNT,
                    QSV_MS_COUNT,
                    &ulms,
                    sizeof(ulms));
    return ulms;
}

/*
 *@@ doshDevIOCtl:
 *
 *      Works with those IOCtls where the buffer
 *      size parameters are always the same anyway,
 *      which applies to all IOCtls I have seen
 *      so far.
 *
 *@@added V0.9.13 (2001-06-14) [umoeller]
 */

APIRET doshDevIOCtl(HFILE hf,
                    ULONG ulCategory,
                    ULONG ulFunction,
                    PVOID pvParams,
                    ULONG cbParams,
                    PVOID pvData,
                    ULONG cbData)
{
    return DosDevIOCtl(hf,
                       ulCategory,
                       ulFunction,
                       pvParams, cbParams, &cbParams,
                       pvData, cbData, &cbData);
}

/*
 *@@category: Helpers\Control program helpers\Shared memory management
 *      helpers for allocating and requesting shared memory.
 */

/* ******************************************************************
 *
 *   Memory helpers
 *
 ********************************************************************/

/*
 *@@ doshMalloc:
 *      wrapper around malloc() which automatically
 *      sets ERROR_NOT_ENOUGH_MEMORY.
 *
 *@@added V0.9.16 (2001-10-19) [umoeller]
 */

PVOID doshMalloc(ULONG cb,
                 APIRET *parc)
{
    PVOID pv;
    *parc = NO_ERROR;
    if (!(pv = malloc(cb)))
        *parc = ERROR_NOT_ENOUGH_MEMORY;

    return pv;
}

/*
 *@@ doshAllocArray:
 *      allocates c * cbArrayItem bytes.
 *      Similar to calloc(), but returns
 *      error codes:
 *
 *      --  NO_ERROR: *ppv and *pcbAllocated were set.
 *
 *      --  ERROR_NO_DATA: either c or cbArrayItem are
 *          zero.
 *
 *      --  ERROR_NOT_ENOUGH_MEMORY: malloc() failed.
 *
 *@@added V0.9.16 (2001-12-08) [umoeller]
 */

APIRET doshAllocArray(ULONG c,              // in: array item count
                      ULONG cbArrayItem,    // in: size of one array item
                      PBYTE *ppv,           // out: memory ptr if NO_ERROR is returned
                      PULONG pcbAllocated)  // out: # of bytes allocated
{
    if (!c || !cbArrayItem)
        return ERROR_NO_DATA;

    *pcbAllocated = c * cbArrayItem;
    if (!(*ppv = (PBYTE)malloc(*pcbAllocated)))
        return ERROR_NOT_ENOUGH_MEMORY;

    return NO_ERROR;
}

/*
 *@@ doshAllocSharedMem:
 *      wrapper for DosAllocSharedMem which has
 *      a malloc()-like syntax. Just due to my
 *      lazyness.
 *
 *      Note that ulSize is always rounded up to the
 *      next 4KB value, so don't use this hundreds of times.
 *
 *      Returns NULL upon errors. Possible errors include
 *      that a memory block calle pcszName has already been
 *      allocated.
 *
 *      Use DosFreeMem(pvrc) to free the memory. The memory
 *      will only be freed if no other process has requested
 *      access.
 *
 *@@added V0.9.3 (2000-04-18) [umoeller]
 */

PVOID doshAllocSharedMem(ULONG ulSize,      // in: requested mem block size (rounded up to 4KB)
                         const char* pcszName) // in: name of block ("\\SHAREMEM\\xxx") or NULL
{
    PVOID   pvrc = NULL;

    if (!DosAllocSharedMem((PVOID*)&pvrc,
                           (PSZ)pcszName,
                           ulSize,
                           PAG_COMMIT | PAG_READ | PAG_WRITE))
        return pvrc;

    return NULL;
}

/*
 *@@ doshRequestSharedMem:
 *      requests access to a block of named shared memory
 *      allocated by doshAllocSharedMem.
 *
 *      Returns NULL upon errors.
 *
 *      Use DosFreeMem(pvrc) to free the memory. The memory
 *      will only be freed if no other process has requested
 *      access.
 *
 *@@added V0.9.3 (2000-04-19) [umoeller]
 */

PVOID doshRequestSharedMem(PCSZ pcszName)
{
    PVOID pvrc = NULL;

    if (!DosGetNamedSharedMem((PVOID*)pvrc,
                              (PSZ)pcszName,
                              PAG_READ | PAG_WRITE))
        return pvrc;

    return NULL;
}

/*
 *@@category: Helpers\Control program helpers\Drive management
 *      functions for managing drives... enumerating, testing,
 *      querying etc.
 */

/* ******************************************************************
 *
 *   Drive helpers
 *
 ********************************************************************/

/*
 *@@ doshIsFixedDisk:
 *      checks whether a disk is fixed or removeable.
 *      ulLogicalDrive must be 1 for drive A:, 2 for B:, ...
 *      The result is stored in *pfFixed.
 *      Returns DOS error code.
 *
 *      From my testing, this function does _not_ provoke
 *      "drive not ready" popups, even if the disk is not
 *      ready.
 *
 *      Warning: This uses DosDevIOCtl, which has proved
 *      to cause problems with some device drivers for
 *      removeable disks.
 *
 *      Returns:
 *
 *      --  NO_ERROR: *pfFixed was set.
 *
 *      --  ERROR_INVALID_DRIVE: drive letter invalid
 *
 *      --  ERROR_NOT_SUPPORTED (50): for network drives.
 *
 *@@changed V0.9.14 (2001-08-03) [umoeller]: added extra fix for A: and B:
 */

APIRET doshIsFixedDisk(ULONG ulLogicalDrive,   // in: 1 for A:, 2 for B:, 3 for C:, ...
                       PBOOL pfFixed)          // out: TRUE for fixed disks
{
    APIRET arc = ERROR_INVALID_DRIVE;

    if (    (ulLogicalDrive == 1)
         || (ulLogicalDrive == 2)
       )
    {
        // drive A: and B: can never be fixed V0.9.14 (2001-08-03) [umoeller]
        *pfFixed = FALSE;
        return NO_ERROR;
    }

    if (ulLogicalDrive)
    {
        // parameter packet
        #pragma pack(1)
        struct {
            UCHAR   command,
                    drive;
        } parms;
        #pragma pack()

        // data packet
        UCHAR ucNonRemoveable;

        parms.drive = (UCHAR)(ulLogicalDrive - 1);
        if (!(arc = doshDevIOCtl((HFILE)-1,
                                 IOCTL_DISK,                  // 0x08
                                 DSK_BLOCKREMOVABLE,          // 0x20
                                 &parms, sizeof(parms),
                                 &ucNonRemoveable, sizeof(ucNonRemoveable))))
            *pfFixed = (BOOL)ucNonRemoveable;
    }

    return arc;
}

/*
 *@@ doshQueryDiskParams:
 *      this retrieves more information about a given drive,
 *      which is stored in the specified BIOSPARAMETERBLOCK
 *      structure.
 *
 *      BIOSPARAMETERBLOCK is defined in the Toolkit headers,
 *      and from my testing, it's the same with the Toolkits
 *      3 and 4.5.
 *
 *      If NO_ERROR is returned, the bDeviceType field can
 *      be one of the following (according to CPREF):
 *
 *      --  0:  48 TPI low-density diskette drive
 *      --  1:  96 TPI high-density diskette drive
 *      --  2:  3.5-inch 720KB diskette drive
 *      --  3:  8-Inch single-density diskette drive
 *      --  4:  8-Inch double-density diskette drive
 *      --  5:  Fixed disk
 *      --  6:  Tape drive
 *      --  7:  Other (includes 1.44MB 3.5-inch diskette drive)
 *      --  8:  R/W optical disk
 *      --  9:  3.5-inch 4.0MB diskette drive (2.88MB formatted)
 *
 *      From my testing, this function does _not_ provoke
 *      "drive not ready" popups, even if the disk is not
 *      ready.
 *
 *      Warning: This uses DosDevIOCtl, which has proved
 *      to cause problems with some device drivers for
 *      removeable disks.
 *
 *      This returns the DOS error code of DosDevIOCtl.
 *      This will be:
 *
 *      --  NO_ERROR for all local disks;
 *
 *      --  ERROR_NOT_SUPPORTED (50) for network drives.
 *
 *@@added V0.9.0 [umoeller]
 *@@changed V0.9.13 (2001-06-14) [umoeller]: changed prototype to use BIOSPARAMETERBLOCK directly
 *@@changed V0.9.13 (2001-06-14) [umoeller]: now querying standard media, no redetermine
 */

APIRET doshQueryDiskParams(ULONG ulLogicalDrive,        // in:  1 for A:, 2 for B:, 3 for C:, ...
                           PBIOSPARAMETERBLOCK pdp)     // out: drive parameters
{
    APIRET arc = ERROR_INVALID_DRIVE;

    if (ulLogicalDrive)
    {
        #pragma pack(1)
        // parameter packet
        struct {
            UCHAR   ucCommand,
                    ucDrive;
        } parms;
        #pragma pack()

        parms.ucCommand = 0;    // 0 = return standard media,
                                // 1 = read currently inserted media
                                // (1 doesn't work any more, returns arc 87
                                // V0.9.13 (2001-06-14) [umoeller])
        parms.ucDrive=(UCHAR)(ulLogicalDrive-1);

        // zero the structure V0.9.13 (2001-06-14) [umoeller]
        memset(pdp, 0, sizeof(BIOSPARAMETERBLOCK));

        arc = doshDevIOCtl((HFILE)-1,
                           IOCTL_DISK,                  // 0x08
                           DSK_GETDEVICEPARAMS,         // 0x63
                           &parms, sizeof(parms),
                           pdp,   sizeof(BIOSPARAMETERBLOCK));

        /* if (!arc)
        {
            _Pmpf(("      bDeviceType: %d", pdp->bDeviceType));
            _Pmpf(("      bytes per sector: %d", pdp->usBytesPerSector));
            _Pmpf(("      sectors per track: %d", pdp->usSectorsPerTrack));
        } */
    }

    return arc;
}

/*
 *@@ doshQueryDriveType:
 *      tests the specified BIOSPARAMETERBLOCK
 *      for whether it represents a CD-ROM or
 *      some other removeable drive type.
 *
 *      Returns one of:
 *
 *      --  DRVTYPE_HARDDISK (0)
 *
 *      --  DRVTYPE_PARTITIONABLEREMOVEABLE
 *
 *      --  DRVTYPE_CDROM
 *
 *      --  DRVTYPE_TAPE
 *
 *      --  DRVTYPE_VDISK
 *
 *      --  DRVTYPE_FLOPPY
 *
 *      --  DRVTYPE_UNKNOWN (255)
 *
 *      The BIOSPARAMETERBLOCK must be filled
 *      first using doshQueryDiskParams.
 *
 *@@added V0.9.16 (2002-01-13) [umoeller]
 */

BYTE doshQueryDriveType(ULONG ulLogicalDrive,
                        PBIOSPARAMETERBLOCK pdp,
                        BOOL fFixed)
{
    if (pdp)
    {
        if (pdp->fsDeviceAttr & DEVATTR_PARTITIONALREMOVEABLE) // 0x08
            return DRVTYPE_PRT;

        if (fFixed)
            return DRVTYPE_HARDDISK;

        if (    (pdp->bDeviceType == 7)     // "other"
             && (pdp->usBytesPerSector == 2048)
             && (pdp->usSectorsPerTrack == (USHORT)-1)
           )
            return DRVTYPE_CDROM;

        switch (pdp->bDeviceType)
        {
            case DEVTYPE_TAPE: // 6
                return DRVTYPE_TAPE;

            case DEVTYPE_48TPI:     // 0, 360k  5.25" floppy
            case DEVTYPE_96TPI:     // 1, 1.2M  5.25" floppy
            case DEVTYPE_35:        // 2, 720k  3.5" floppy
            case DEVTYPE_OTHER:     // 7, 1.44  3.5" floppy
                                    //    1.84M 3.5" floppy
            case DEVTYPE_35_288MB:
                if (    (ulLogicalDrive == 1)
                     || (ulLogicalDrive == 2)
                   )
                    return DRVTYPE_FLOPPY;

                return DRVTYPE_VDISK;

            case DEVTYPE_RWOPTICAL: // 8, what is this?!?
                return DRVTYPE_FLOPPY;
        }
    }

    return DRVTYPE_UNKNOWN;
}

/*
 *@@ doshQueryCDDrives:
 *      returns the no. of CD-ROM drives on the system
 *      as well as the drive letter of the first
 *      CD-ROM drive.
 *
 *@@added V1.0.0 (2002-08-31) [umoeller]
 */

APIRET doshQueryCDDrives(PBYTE pcCDs,           // out: CD-ROM drives count
                         PCHAR pcFirstCD)       // out: drive letter of first CD
{
    APIRET  arc;
    HFILE   hfCDROM;
    ULONG   ulAction;

    if (!(arc = DosOpen("\\DEV\\CD-ROM2$",
                        &hfCDROM,
                        &ulAction,
                        0,
                        FILE_NORMAL,
                        OPEN_ACTION_OPEN_IF_EXISTS,
                        OPEN_SHARE_DENYNONE | OPEN_ACCESS_READONLY,
                        NULL)))
    {
        struct
        {
            USHORT cCDs;
            USHORT usFirstCD;       // 0 == A:, 1 == B:, ...
        } cdinfo;

        ULONG cb = sizeof(cdinfo);

        if (!(arc = DosDevIOCtl(hfCDROM,
                                0x82,
                                0x60,
                                NULL,
                                0,
                                NULL,
                                &cdinfo,
                                cb,
                                &cb)))
        {
            *pcCDs = cdinfo.cCDs;
            *pcFirstCD = cdinfo.usFirstCD + 'A';
        }

        DosClose(hfCDROM);
    }

    return arc;
}

/*
 *@@ doshOpenDrive:
 *      opens the given logical drive using
 *      DosOpen with OPEN_FLAGS_DASD. Use
 *      the file handle returned from here
 *      for doshHasAudioCD and doshQueryCDStatus.
 *
 *      If NO_ERROR is returned, use DosClose
 *      to close the device again.
 *
 *@@added V1.0.0 (2002-08-31) [umoeller]
 */

APIRET doshOpenDrive(ULONG ulLogicalDrive,
                     HFILE *phf)            // out: open drive's file handle
{
    ULONG   dummy;

    CHAR    szDrive[] = "C:";
    szDrive[0] = 'A' + ulLogicalDrive - 1;

    return DosOpen(szDrive,   // "C:", "D:", ...
                   phf,
                   &dummy,
                   0,
                   FILE_NORMAL,
                   // OPEN_ACTION_FAIL_IF_NEW
                          OPEN_ACTION_OPEN_IF_EXISTS,
                   OPEN_FLAGS_DASD
                          | OPEN_FLAGS_FAIL_ON_ERROR
                                // ^^^ if this flag is not set, we get the white
                                // hard-error box
                          | OPEN_FLAGS_NOINHERIT     // V0.9.6 (2000-11-25) [pr]
               //            | OPEN_ACCESS_READONLY  // V0.9.13 (2001-06-14) [umoeller]
                          | OPEN_SHARE_DENYNONE,
                   NULL);
}

/*
 *@@ doshHasAudioCD:
 *      sets *pfAudio to whether ulLogicalDrive
 *      currently has an audio CD inserted.
 *
 *      Better call this only if you're sure that
 *      ulLogicalDrive is a CD-ROM drive. Use
 *      doshQueryRemoveableType to check.
 *
 *@@added V0.9.14 (2001-08-01) [umoeller]
 *@@changed V1.0.0 (2002-08-31) [umoeller]: removed ulLogicalDrive which was not needed
 */

APIRET doshHasAudioCD(HFILE hfDrive,            // in: DASD open
                      BOOL fMixedModeCD,
                      PBOOL pfAudio)
{
    APIRET  arc = NO_ERROR;

    ULONG ulAudioTracks = 0,
          ulDataTracks = 0;

    CHAR cds1[4] = { 'C', 'D', '0', '1' };
    CHAR cds2[4];

    *pfAudio = FALSE;

    // check for proper driver signature
    if (!(arc = doshDevIOCtl(hfDrive,
                             IOCTL_CDROMDISK,
                             CDROMDISK_GETDRIVER,
                             &cds1, sizeof(cds1),
                             &cds2, sizeof(cds2))))
    {
        if (memcmp(&cds1, &cds2, 4))
            // this is not a CD-ROM then:
            arc = NO_ERROR;
        else
        {
            #pragma pack(1)         // V1.0.0 (2002-08-31) [umoeller]

            struct {
                UCHAR   ucFirstTrack,
                        ucLastTrack;
                ULONG   ulLeadOut;
            } cdat;

            struct {
                ULONG   ulTrackAddress;
                BYTE    bFlags;
            } trackdata;

            #pragma pack()

            // get track count
            if (!(arc = doshDevIOCtl(hfDrive,
                                     IOCTL_CDROMAUDIO,
                                     CDROMAUDIO_GETAUDIODISK,
                                     &cds1, sizeof(cds1),
                                     &cdat, sizeof(cdat))))
            {
                // still no error: build the audio TOC
                ULONG i;
                for (i = cdat.ucFirstTrack;
                     i <= cdat.ucLastTrack;
                     i++)
                {
                    BYTE cdtp[5] =
                      { 'C', 'D', '0', '1', (UCHAR)i };

                    if (!(arc = doshDevIOCtl(hfDrive,
                                             IOCTL_CDROMAUDIO,
                                             CDROMAUDIO_GETAUDIOTRACK,
                                             &cdtp, sizeof(cdtp),
                                             &trackdata, sizeof(trackdata))))
                    {
                        if (trackdata.bFlags & 64)
                            ulDataTracks++;
                        else
                        {
                            ulAudioTracks++;

                            if (!fMixedModeCD)
                            {
                                // caller doesn't want mixed mode:
                                // stop here
                                ulDataTracks = 0;
                                break;
                            }
                        }
                    }
                }

                // _Pmpf(("   got %d audio, %d data tracks",
                //             ulAudioTracks, ulDataTracks));

                if (!ulDataTracks)
                    *pfAudio = TRUE;
            }
            else
            {
                // not audio disk:
                // go on then
                // _Pmpf(("   CDROMAUDIO_GETAUDIODISK returned %d", arc));
                arc = NO_ERROR;
            }
        }
    }
    else
    {
        // not CD-ROM: go on then
        // _Pmpf(("   CDROMDISK_GETDRIVER returned %d", arc));
        arc = NO_ERROR;
    }

    return arc;
}

/*
 *@@ doshQueryCDStatus:
 *      returns the status bits of a CD-ROM drive.
 *      This calls the CDROMDISK_DEVICESTATUS
 *      ioctl.
 *
 *      If NO_ERROR is returned, *pflStatus has
 *      received the following flags:
 *
 *      --  CDFL_DOOROPEN (bit 0)
 *
 *      --  CDFL_DOORLOCKED (bit 1)
 *
 *      and many more (see dosh.h).
 *
 *      Actually I wrote this function to have a way to
 *      find out whether the drive door is already open.
 *      But thanks to IBM's thoughtful design, this ioctl
 *      is 99% useless for that purpose since it requires
 *      a DASD disk handle to be passed in, which cannot
 *      be obtained if there's no media in the drive.
 *
 *      In other words, it is absolutely impossible to
 *      ever get the CDFL_DOOROPEN flag, because if the
 *      door is open, DosOpen already fails on the drive.
 *      As a consequence, it is seems to be impossible
 *      to find out if the door is open with OS/2.
 *
 *@@added V1.0.0 (2002-08-31) [umoeller]
 */

APIRET doshQueryCDStatus(HFILE hfDrive,            // in: DASD open
                         PULONG pflStatus)         // out: CD-ROM status bits
{
    APIRET  arc;

    CHAR    cds1[4] = { 'C', 'D', '0', '1' };
    ULONG   fl;

    *pflStatus = 0;

    if (!(arc = doshDevIOCtl(hfDrive,
                             IOCTL_CDROMDISK,
                             CDROMDISK_DEVICESTATUS,
                             &cds1, sizeof(cds1),
                             &fl, sizeof(fl))))
    {
        *pflStatus = fl;
    }

    return arc;
}

/*
 *@@ doshEnumDrives:
 *      this function enumerates all valid drive letters on
 *      the system by composing a string of drive letters
 *      in the buffer pointed to by pszBuffer, which should
 *      be 27 characters in size to hold information for
 *      all drives. The buffer will be null-terminated.
 *
 *      If (pcszFileSystem != NULL), only drives matching
 *      the specified file system type (e.g. "HPFS") will
 *      be enumerated. If (pcszFileSystem == NULL), all
 *      drives will be enumerated.
 *
 *      If (fSkipRemovables == TRUE), removeable drives will
 *      be skipped. This applies to floppy, CD-ROM, and
 *      virtual floppy drives. This will start the search
 *      at drive letter C: so that drives A: and B: will
 *      never be checked (to avoid the hardware bumps).
 *
 *      Otherwise, the search starts at drive A:. Still,
 *      removeable drives will only be added if valid media
 *      is inserted.
 *
 *@@changed V0.9.4 (2000-07-03) [umoeller]: this stopped at the first invalid drive letter; fixed
 *@@changed V0.9.4 (2000-07-03) [umoeller]: added fSkipRemoveables
 */

VOID doshEnumDrives(PSZ pszBuffer,      // out: drive letters
                    PCSZ pcszFileSystem,  // in: FS's to match or NULL
                    BOOL fSkipRemoveables) // in: if TRUE, only non-removeable disks will be returned
{
    CHAR    szName[5] = "";
    ULONG   ulLogicalDrive = 1, // start with drive A:
            ulFound = 0;        // found drives count
    APIRET  arc = NO_ERROR; // return code

    if (fSkipRemoveables)
        // start with drive C:
        ulLogicalDrive = 3;

    // go thru the drives, start with C: (== 3), stop after Z: (== 26)
    while (ulLogicalDrive <= 26)
    {
        #pragma pack(1)
        struct
        {
            UCHAR dummy,drive;
        } parms;
        #pragma pack()

        // data packet
        UCHAR nonRemovable=0;

        parms.drive=(UCHAR)(ulLogicalDrive-1);
        arc = doshDevIOCtl((HFILE)-1,
                           IOCTL_DISK,
                           DSK_BLOCKREMOVABLE,
                           &parms, sizeof(parms),
                           &nonRemovable, sizeof(nonRemovable));

        if (    // fixed disk and non-removeable
                ((arc == NO_ERROR) && (nonRemovable))
                // or network drive:
             || (arc == ERROR_NOT_SUPPORTED)
           )
        {
            ULONG  ulOrdinal       = 0;     // ordinal of entry in name list
            BYTE   fsqBuffer[sizeof(FSQBUFFER2) + (3 * CCHMAXPATH)] = {0};
            ULONG  cbBuffer   = sizeof(fsqBuffer);        // Buffer length)
            PFSQBUFFER2 pfsqBuffer = (PFSQBUFFER2)fsqBuffer;

            szName[0] = ulLogicalDrive + 'A' - 1;
            szName[1] = ':';
            szName[2] = '\0';

            arc = DosQueryFSAttach(szName,          // logical drive of attached FS
                                   ulOrdinal,       // ignored for FSAIL_QUERYNAME
                                   FSAIL_QUERYNAME, // return data for a Drive or Device
                                   pfsqBuffer,      // returned data
                                   &cbBuffer);      // returned data length

            if (arc == NO_ERROR)
            {
                // The data for the last three fields in the FSQBUFFER2
                // structure are stored at the offset of fsqBuffer.szName.
                // Each data field following fsqBuffer.szName begins
                // immediately after the previous item.
                CHAR* pszFSDName = (PSZ)&(pfsqBuffer->szName) + (pfsqBuffer->cbName) + 1;
                if (pcszFileSystem == NULL)
                {
                    // enum-all mode: always copy
                    pszBuffer[ulFound] = szName[0]; // drive letter
                    ulFound++;
                }
                else if (strcmp(pszFSDName, pcszFileSystem) == 0)
                {
                    pszBuffer[ulFound] = szName[0]; // drive letter
                    ulFound++;
                }
            }
        }

        ulLogicalDrive++;
    } // end while (G_acDriveLetters[ulLogicalDrive] <= 'Z')

    pszBuffer[ulFound] = '\0';
}

/*
 *@@ doshQueryBootDrive:
 *      returns the letter of the boot drive as a
 *      single (capital) character, which is useful for
 *      constructing file names using sprintf and such.
 *
 *@@changed V0.9.16 (2002-01-13) [umoeller]: optimized
 */

CHAR doshQueryBootDrive(VOID)
{
    // this can never change, so query this only once
    // V0.9.16 (2002-01-13) [umoeller]
    static CHAR     cBootDrive = '\0';

    if (!cBootDrive)
    {
        ULONG ulBootDrive;
        DosQuerySysInfo(QSV_BOOT_DRIVE, QSV_BOOT_DRIVE,
                        &ulBootDrive,
                        sizeof(ulBootDrive));
        cBootDrive = (CHAR)ulBootDrive + 'A' - 1;
    }

    return cBootDrive;
}

/*
 *@@ doshQueryMedia:
 *      determines whether the given drive currently
 *      has media inserted.
 *
 *      Call this only for non-fixed (removable) disks.
 *      Use doshIsFixedDisk to find out.
 *
 *      Returns:
 *
 *      --  NO_ERROR: media is present.
 *
 *      --  ERROR_AUDIO_CD_ROM (10000): audio CD-ROM is present.
 *
 *      --  ERROR_NOT_READY (21) or other: drive has no media.
 *
 *@@added V0.9.16 (2002-01-13) [umoeller]
 */

APIRET doshQueryMedia(ULONG ulLogicalDrive,    // in: 1 for A:, 2 for B:, 3 for C:, ...
                      BOOL fCDROM,             // in: is drive CD-ROM?
                      ULONG fl)                // in: DRVFL_* flags
{
    APIRET  arc;

    HFILE   hf;

    // exported this code to doshOpenDrive V1.0.0 (2002-08-31) [umoeller]
    arc = doshOpenDrive(ulLogicalDrive,
                        &hf);

    // this still returns NO_ERROR for audio CDs in a
    // CD-ROM drive...
    // however, the WPS then attempts to read in the
    // root directory for audio CDs, which produces
    // a "sector not found" error box...

    if (    (!arc)
         && (hf)
         && (fCDROM)
       )
    {
        BOOL fAudio;
        if (    (!(arc = doshHasAudioCD(hf,
                                        ((fl & DRVFL_MIXEDMODECD) != 0),
                                        &fAudio)))
             && (fAudio)
           )
            arc = ERROR_AUDIO_CD_ROM;       // special private error code (10000)
    }

    if (hf)
        DosClose(hf);

    return arc;
}

/*
 *@@ doshAssertDrive:
 *      this checks for whether the given drive
 *      is currently available without provoking
 *      those ugly white "Drive not ready" popups.
 *
 *      "fl" can specify additional flags for testing
 *      and can be any combination of:
 *
 *      --  DRVFL_MIXEDMODECD: whether to allow
 *          mixed-mode CD-ROMs. See error codes below.
 *
 *      This returns (from my testing):
 *
 *      -- NO_ERROR: drive is available.
 *
 *      -- ERROR_INVALID_DRIVE (15): drive letter does not exist.
 *
 *      -- ERROR_NOT_READY (21): drive exists, but is not ready.
 *                  This is produced by floppies and CD-ROM drives
 *                  which do not have valid media inserted.
 *
 *      -- ERROR_AUDIO_CD_ROM (10000): special error code returned
 *                  only by this function if a CD-ROM drive has audio
 *                  media inserted.
 *
 *                  If DRVFL_MIXEDMODECD was specified, ERROR_AUDIO_CD_ROM
 *                  is returned _only_ if _no_ data tracks are
 *                  present on a CD-ROM. Since OS/2 is not very
 *                  good at handling mixed-mode CDs, this might not
 *                  be desireable.
 *
 *                  If DRVFL_MIXEDMODECD was not set, ERROR_AUDIO_CD_ROM
 *                  will be returned already if _one_ audio track is present.
 *
 *@@changed V0.9.1 (99-12-13) [umoeller]: rewritten, prototype changed. Now using DosOpen on the drive instead of DosError.
 *@@changed V0.9.1 (2000-01-08) [umoeller]: DosClose was called even if DosOpen failed, which messed up OS/2 error handling.
 *@@changed V0.9.1 (2000-02-09) [umoeller]: this didn't work for network drives, including RAMFS; fixed.
 *@@changed V0.9.3 (2000-03-28) [umoeller]: added check for network drives, which weren't working
 *@@changed V0.9.4 (2000-08-03) [umoeller]: more network fixes
 *@@changed V0.9.9 (2001-03-19) [pr]: validate drive number
 *@@changed V0.9.11 (2001-04-23) [umoeller]: added an extra check for floppies
 *@@changed V0.9.13 (2001-06-14) [umoeller]: added "fl" parameter and lots of CD-ROM checks
 */

APIRET doshAssertDrive(ULONG ulLogicalDrive,    // in: 1 for A:, 2 for B:, 3 for C:, ...
                       ULONG fl)                // in: DRVFL_* flags
{
    APIRET  arc = NO_ERROR;
    BOOL    fFixed = FALSE,
            fCDROM = FALSE;

    if ((ulLogicalDrive < 1) || (ulLogicalDrive > 26))
        return(ERROR_PATH_NOT_FOUND);

    arc = doshIsFixedDisk(ulLogicalDrive,
                          &fFixed);    // V0.9.13 (2001-06-14) [umoeller]

    // _Pmpf((__FUNCTION__ ": doshIsFixedDisk returned %d for disk %d", arc, ulLogicalDrive));
    // _Pmpf(("   fFixed is %d", fFixed));

    if (!arc)
        if (!fFixed)
        {
            // removeable disk:
            // check if it's a CD-ROM
            BIOSPARAMETERBLOCK bpb;
            arc = doshQueryDiskParams(ulLogicalDrive,
                                      &bpb);
            // _Pmpf(("   doshQueryDiskParams returned %d", arc));

            if (    (!arc)
                 && (DRVTYPE_CDROM == doshQueryDriveType(ulLogicalDrive,
                                                         &bpb,
                                                         fFixed))
               )
            {
                // _Pmpf(("   --> is CD-ROM"));
                fCDROM = TRUE;
            }
        }

    if (!arc)
        arc = doshQueryMedia(ulLogicalDrive,
                             fCDROM,
                             fl);

    switch (arc)
    {
        case ERROR_NETWORK_ACCESS_DENIED: // 65
            // added V0.9.3 (2000-03-27) [umoeller];
            // according to user reports, this is returned
            // by all network drives, which apparently don't
            // support DASD DosOpen
        case ERROR_ACCESS_DENIED: // 5
            // added V0.9.4 (2000-07-10) [umoeller]
            // LAN drives still didn't work... apparently
            // the above only works for NFS drives
        case ERROR_PATH_NOT_FOUND: // 3
            // added V0.9.4 (2000-08-03) [umoeller]:
            // this is returned by some other network types...
            // sigh...
        case ERROR_NOT_SUPPORTED: // 50
            // this is returned by file systems which don't
            // support DASD DosOpen;
            // use some other method then, this isn't likely
            // to fail -- V0.9.1 (2000-02-09) [umoeller]

            // but don't do this for floppies
            // V0.9.11 (2001-04-23) [umoeller]
            if (ulLogicalDrive > 2)
            {
                FSALLOCATE  fsa;
                arc = DosQueryFSInfo(ulLogicalDrive,
                                     FSIL_ALLOC,
                                     &fsa,
                                     sizeof(fsa));
                // _Pmpf(("   re-checked, DosQueryFSInfo returned %d", arc));
            }
        break;
    }

    return arc;
}

/*
 *@@ doshGetDriveInfo:
 *      fills the given XDISKINFO buffer with
 *      information about the given logical drive.
 *
 *      This function will not provoke "Drive not
 *      ready" popups, hopefully.
 *
 *      fl can be any combination of the following:
 *
 *      --  DRVFL_MIXEDMODECD: see doshAssertDrive.
 *
 *      --  DRVFL_TOUCHFLOPPIES: drive A: and B: should
 *          be touched for media checks (click, click);
 *          otherwise they will be left alone and
 *          default values will be returned.
 *
 *      --  DRVFL_CHECKEAS: drive should always be
 *          checked for EA support. If this is set,
 *          we will call DosFSCtl for the non-well-known
 *          file systems so we will always have a
 *          value for the DFL_SUPPORTS_EAS flags.
 *          The EA support returned by DosFSCtl
 *          might not be correct for remote file
 *          systems since not all of them support
 *          that query.
 *
 *          If not set, we set DFL_SUPPORTS_EAS only
 *          for file systems such as HPFS and JFS
 *          that are known to support EAs.
 *
 *      --  DRVFL_CHECKLONGNAMES: drive should be
 *          tested for longname support. If this is
 *          set, we will try a DosOpen("\\long.name.file")
 *          on the drive to see if it supports long
 *          filenames (unless it's a "well-known"
 *          file-system and we know it does). If enabled,
 *          the DFL_SUPPORTS_LONGNAMES flag is reliable.
 *          Note that this does not check for what special
 *          characters are supported in file names.
 *
 *      This should return only one of the following:
 *
 *      --  NO_ERROR: disk info was filled, but not
 *          necessarily all info was available (e.g.
 *          if no media was present in CD-ROM drive).
 *          See remarks below.
 *
 *      --  ERROR_INVALID_DRIVE 15): ulLogicalDrive
 *          is not used at all (invalid drive letter)
 *
 *      --  ERROR_BAD_UNIT (20): if drive was renamed for
 *          some reason (according to user reports
 *
 *      --  ERROR_NOT_READY (21): for ZIP disks where
 *          no media is inserted, depending on the
 *          driver apparently... normally ZIP drive
 *          letters should disappear when no media
 *          is present
 *
 *      --  ERROR_DRIVE_LOCKED (108)
 *
 *      So in order to check whether a drive is present
 *      and available, use this function as follows:
 *
 *      1)  Call this function and check whether it
 *          returns NO_ERROR for the given drive.
 *          This will rule out invalid drive letters
 *          and drives that are presently locked by
 *          CHKDSK or something.
 *
 *      2)  If so, check whether XDISKINFO.flDevice
 *          has the DFL_MEDIA_PRESENT flag set.
 *          This will rule out removeable drives without
 *          media and unformatted hard disks.
 *
 *      3)  If so, you can test the other fields if
 *          you need more information. For example,
 *          it would not be a good idea to create
 *          a new file if the bType field is
 *          DRVTYPE_CDROM.
 *
 *          If you want to exclude removeable disks,
 *          instead of checking bType, you should
 *          rather check flDevice for the DFL_FIXED
 *          flag, which will be set for ZIP drives also.
 *
 *      Remarks for special drive types:
 *
 *      --  Hard disks always have bType == DRVTYPE_HARDDISK.
 *          For them, we always check the file system.
 *          If this is reported as "UNKNOWN", this means
 *          that the drive is unformatted or formatted
 *          with a file system that OS/2 does not understand
 *          (e.g. NTFS). Only in that case, flDevice
 *          has the DFL_MEDIA_PRESENT bit clear.
 *
 *          DFL_FIXED is always set.
 *
 *      --  Remote (LAN) drives always have bType == DRVTYPE_LAN.
 *          flDevice will always have the DFL_REMOTE and
 *          DFL_MEDIA_PRESENT bits set.
 *
 *      --  ZIP disks will have bType == DRVTYPE_PARTITIONABLEREMOVEABLE.
 *          For them, flDevice will have both the
 *          and DFL_PARTITIONABLEREMOVEABLE and DFL_FIXED
 *          bits set.
 *
 *          ZIP disks are a bit special because they are
 *          dynamically mounted and unmounted when media
 *          is inserted and removed. In other words, if
 *          no media is present, the drive letter becomes
 *          invalid.
 *
 *      --  CD-ROM and DVD drives and CD writers will always
 *          be reported as DRVTYPE_CDROM. The DFL_FIXED bit
 *          will be clear always. For them, always check the
 *          DFL_MEDIA_PRESENT present bit to avoid "Drive not
 *          ready" popups.
 *
 *          As a special goody, we can also determine if the
 *          drive currently has audio media inserted (which
 *          would provoke errors also), by setting the
 *          DFL_AUDIO_CD bit.
 *
 *@@added V0.9.16 (2002-01-13) [umoeller]
 *@@changed V0.9.19 (2002-04-25) [umoeller]: added CDWFS (RSJ CD-Writer)
 */

APIRET doshGetDriveInfo(ULONG ulLogicalDrive,
                        ULONG fl,               // in: DRVFL_* flags
                        PXDISKINFO pdi)
{
    APIRET  arc = NO_ERROR;

    HFILE   hf;
    ULONG   dummy;
    BOOL    fCheck = TRUE,
            fCheckFS = FALSE,
            fCheckLongnames = FALSE,
            fCheckEAs = FALSE;

    memset(pdi, 0, sizeof(XDISKINFO));

    pdi->cDriveLetter = 'A' + ulLogicalDrive - 1;
    pdi->cLogicalDrive = ulLogicalDrive;

    pdi->bType = DRVTYPE_UNKNOWN;
    pdi->fPresent = TRUE;       // for now

    if (    (ulLogicalDrive == 1)
         || (ulLogicalDrive == 2)
       )
    {
        // drive A: and B: are special cases,
        // we don't even want to touch them (click, click)
        pdi->bType = DRVTYPE_FLOPPY;

        if (0 == (fl & DRVFL_TOUCHFLOPPIES))
        {
            fCheck = FALSE;
            // these support EAs too
            pdi->flDevice  = DFL_MEDIA_PRESENT | DFL_SUPPORTS_EAS;
            strcpy(pdi->szFileSystem, "FAT");
            pdi->lFileSystem = FSYS_FAT;
        }
    }

    if (fCheck)
    {
        // any other drive:
        // check if it's removeable first
        BOOL    fFixed = FALSE;
        arc = doshIsFixedDisk(ulLogicalDrive,
                              &fFixed);

        switch (arc)
        {
            case ERROR_INVALID_DRIVE:
                // drive letter doesn't exist at all:
                pdi->fPresent = FALSE;
                // return this APIRET
            break;

            case ERROR_NOT_SUPPORTED:       // 50 for network drives
                // we get this for remote drives added
                // via "net use", so set these flags
                pdi->bType = DRVTYPE_LAN;
                pdi->lFileSystem = FSYS_REMOTE;
                pdi->flDevice |= DFL_REMOTE | DFL_MEDIA_PRESENT;
                // but still check what file-system we
                // have and whether longnames are supported
                fCheckFS = TRUE;
                fCheckLongnames = TRUE;
                fCheckEAs = TRUE;
            break;

            case NO_ERROR:
            {
                if (fFixed)
                {
                    // fixed drive:
                    pdi->flDevice |= DFL_FIXED | DFL_MEDIA_PRESENT;

                    fCheckFS = TRUE;
                    fCheckLongnames = TRUE;
                    fCheckEAs = TRUE;
                }

                if (!(arc = doshQueryDiskParams(ulLogicalDrive,
                                                &pdi->bpb)))
                {
                    BYTE bTemp = doshQueryDriveType(ulLogicalDrive,
                                                    &pdi->bpb,
                                                    fFixed);
                    if (bTemp != DRVTYPE_UNKNOWN)
                    {
                        // recognized: store it then
                        pdi->bType = bTemp;

                        if (bTemp == DRVTYPE_PRT)
                            pdi->flDevice |=    DFL_FIXED
                                              | DFL_PARTITIONABLEREMOVEABLE;
                    }

                    if (!fFixed)
                    {
                        // removeable:

                        // before checking the drive, try if we have media
                        if (!(arc = doshQueryMedia(ulLogicalDrive,
                                                   (pdi->bType == DRVTYPE_CDROM),
                                                   fl)))
                        {
                            pdi->flDevice |= DFL_MEDIA_PRESENT;
                            fCheckFS = TRUE;
                            fCheckLongnames = TRUE;
                                    // but never EAs
                        }
                        else if (arc == ERROR_AUDIO_CD_ROM)
                        {
                            pdi->flDevice |= DFL_AUDIO_CD;
                            // do not check longnames and file-system
                        }
                        else
                            pdi->arcQueryMedia = arc;

                        arc = NO_ERROR;
                    }
                }
                else
                    pdi->arcQueryDiskParams = arc;
            }
            break;

            default:
                pdi->arcIsFixedDisk = arc;
                // and return this
            break;

        } // end swich arc = doshIsFixedDisk(ulLogicalDrive, &fFixed);
    }

    if (fCheckFS)
    {
        // TRUE only for local fixed disks or
        // remote drives or if media was present above
        if (!(arc = doshQueryDiskFSType(ulLogicalDrive,
                                        pdi->szFileSystem,
                                        sizeof(pdi->szFileSystem))))
        {
            if (!stricmp(pdi->szFileSystem, "UNKNOWN"))
            {
                // this is returned by the stupid DosQueryFSAttach
                // if the file system is not recognized by OS/2,
                // or if the drive is unformatted
                pdi->lFileSystem = FSYS_UNKNOWN;
                pdi->flDevice &= ~DFL_MEDIA_PRESENT;
                fCheckLongnames = FALSE;
                fCheckEAs = FALSE;
                        // should we return ERROR_NOT_DOS_DISK (26)
                        // in this case?
            }
            else if (!stricmp(pdi->szFileSystem, "FAT"))
            {
                pdi->lFileSystem = FSYS_FAT;
                pdi->flDevice |= DFL_SUPPORTS_EAS;
                fCheckLongnames = FALSE;
                fCheckEAs = FALSE;
            }
            else if (    (!stricmp(pdi->szFileSystem, "HPFS"))
                      || (!stricmp(pdi->szFileSystem, "JFS"))
                    )
            {
                pdi->lFileSystem = FSYS_HPFS_JFS;
                pdi->flDevice |= DFL_SUPPORTS_EAS | DFL_SUPPORTS_LONGNAMES;
                fCheckLongnames = FALSE;
                fCheckEAs = FALSE;
            }
            else if (!stricmp(pdi->szFileSystem, "CDFS"))
                pdi->lFileSystem = FSYS_CDFS;
            else if (    (!stricmp(pdi->szFileSystem, "FAT32"))
                      || (!stricmp(pdi->szFileSystem, "ext2"))
                    )
            {
                pdi->lFileSystem = FSYS_FAT32_EXT2;
                fCheckLongnames = TRUE;
                fCheckEAs = TRUE;
            }
            else if (!stricmp(pdi->szFileSystem, "RAMFS"))
            {
                pdi->lFileSystem = FSYS_RAMFS;
                pdi->flDevice |= DFL_SUPPORTS_EAS | DFL_SUPPORTS_LONGNAMES;
                fCheckLongnames = FALSE;
                fCheckEAs = FALSE;
            }
            else if (!stricmp(pdi->szFileSystem, "TVFS"))
            {
                pdi->lFileSystem = FSYS_TVFS;
                fCheckLongnames = TRUE;
                fCheckEAs = TRUE;
            }
            else if (!stricmp(pdi->szFileSystem, "CDWFS"))
                    // V0.9.19 (2002-04-25) [umoeller]
            {
                pdi->lFileSystem = FSYS_CDWFS;
                pdi->flDevice |= DFL_SUPPORTS_LONGNAMES;
                fCheckLongnames = FALSE;
                fCheckEAs = FALSE;
            }
        }
        else
            // store negative error code
            pdi->lFileSystem = -(LONG)arc;
    }

    if (    (!arc)
         && (fCheckLongnames)
         && (fl & DRVFL_CHECKLONGNAMES)
       )
    {
        CHAR szTemp[] = "?:\\long.name.file";
        szTemp[0]  = ulLogicalDrive + 'A' - 1;
        if (!(arc = DosOpen(szTemp,
                            &hf,
                            &dummy,
                            0,
                            0,
                            FILE_READONLY,
                            OPEN_SHARE_DENYNONE | OPEN_FLAGS_NOINHERIT,
                            0)))
        {
            DosClose(hf);
        }

        switch (arc)
        {
            case NO_ERROR:
            case ERROR_OPEN_FAILED:
            case ERROR_FILE_NOT_FOUND:      // returned by TVFS
                pdi->flDevice |= DFL_SUPPORTS_LONGNAMES;
            break;

            // if longnames are not supported,
            // we get ERROR_INVALID_NAME
            default:
                pdi->arcOpenLongnames = arc;
            break;

            // default:
               //  printf("      drive %d returned %d\n", ulLogicalDrive, arc);
        }

        arc = NO_ERROR;
    }

    if (    (!arc)
         && (fCheckEAs)
         && (fl & DRVFL_CHECKEAS)
       )
    {
        EASIZEBUF easb = {0};
        ULONG   cbData = sizeof(easb),
                cbParams = 0;
        CHAR    szDrive[] = "?:\\";
        szDrive[0] = pdi->cDriveLetter;
        if (!(arc = DosFSCtl(&easb,
                             cbData,
                             &cbData,
                             NULL, // params,
                             cbParams,
                             &cbParams,
                             FSCTL_MAX_EASIZE,
                             szDrive,
                             -1,        // HFILE
                             FSCTL_PATHNAME)))
            if (easb.cbMaxEASize != 0)
                        // the other field (cbMaxEAListSize) is 0 always, I think
                pdi->flDevice |= DFL_SUPPORTS_EAS;
    }

    if (doshQueryBootDrive() == pdi->cDriveLetter)
        pdi->flDevice |= DFL_BOOTDRIVE;

    return arc;
}

/*
 *@@ doshSetLogicalMap:
 *      sets the mapping of logical floppy drives onto a single
 *      physical floppy drive.
 *      This means selecting either drive A: or drive B: to refer
 *      to the physical drive.
 *
 *      Paul explained this to me as follows:
 *
 *      "It is really very simple - in a single physical floppy
 *      drive system,  you still have 2 logical floppy drives
 *      A: and B:. This was primarily to  support disk copying
 *      e.g. diskcopy a: b: on a single floppy system without
 *      having to rewrite applications. Whenever the application
 *      accessed the other logical drive, the user would get a
 *      prompt from the OS to swap disks.
 *
 *      "These calls allow applications to bypass the prompt by
 *      doing the mapping themselves. They just get/set which
 *      logical drive is currently mapped to the physical drive.
 *      This concept existed in DOS as well although the specifics
 *      of how it was done escape me now.... actually I just
 *      looked it up - the byte at 0:504h in low memory on
 *      DOS controlled this (it doesn't work in VDMs)."
 *
 *@@added V0.9.6 (2000-11-24) [pr]
 */

APIRET doshSetLogicalMap(ULONG ulLogicalDrive)
{
    CHAR    name[3] = "?:";
    ULONG   fd = 0,
            action = 0;
//             paramsize = 0;
//             datasize = 0;
    APIRET  rc = NO_ERROR;
    USHORT  data,
            param;

    name[0] = doshQueryBootDrive();
    rc = DosOpen(name,
                 &fd,
                 &action,
                 0,
                 0,
                 OPEN_ACTION_FAIL_IF_NEW
                          | OPEN_ACTION_OPEN_IF_EXISTS,
                 OPEN_FLAGS_DASD
                       | OPEN_FLAGS_FAIL_ON_ERROR
                       | OPEN_FLAGS_NOINHERIT
                       | OPEN_ACCESS_READONLY
                       | OPEN_SHARE_DENYNONE,
                 0);

    if (rc == NO_ERROR)
    {
        param = 0;
        data = (USHORT)ulLogicalDrive;
        // paramsize = sizeof(param);
        // datasize = sizeof(data);
        rc = doshDevIOCtl(fd,
                          IOCTL_DISK, DSK_SETLOGICALMAP,
                          &param, sizeof(param),
                          &data, sizeof(data));
        DosClose(fd);
    }

    return(rc);
}

/*
 *@@ doshQueryDiskSize:
 *      returns the size of the specified disk in bytes.
 *
 *      Note: This returns a "double" value, because a ULONG
 *      can only hold values of some 4 billion, which would
 *      lead to funny results for drives > 4 GB.
 *
 *@@added V0.9.11 (2001-04-18) [umoeller]
 */

APIRET doshQueryDiskSize(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ...
                         double *pdSize)
{
    APIRET      arc = NO_ERROR;
    FSALLOCATE  fsa;
    // double      dbl = -1;

    if (!(arc = DosQueryFSInfo(ulLogicalDrive, FSIL_ALLOC, &fsa, sizeof(fsa))))
        *pdSize = ((double)fsa.cSectorUnit * fsa.cbSector * fsa.cUnit);

    return arc;
}

/*
 *@@ doshQueryDiskFree:
 *      returns the number of bytes remaining on the disk
 *      specified by the given logical drive.
 *
 *      Note: This returns a "double" value, because a ULONG
 *      can only hold values of some 4 billion, which would
 *      lead to funny results for drives > 4 GB.
 *
 *@@changed V0.9.0 [umoeller]: fixed another > 4 GB bug (thanks to Rdiger Ihle)
 *@@changed V0.9.7 (2000-12-01) [umoeller]: changed prototype
 */

APIRET doshQueryDiskFree(ULONG ulLogicalDrive, // in: 1 for A:, 2 for B:, 3 for C:, ...
                         double *pdFree)
{
    APIRET      arc = NO_ERROR;
    FSALLOCATE  fsa;
    // double      dbl = -1;

    if (!(arc = DosQueryFSInfo(ulLogicalDrive, FSIL_ALLOC, &fsa, sizeof(fsa))))
        *pdFree = ((double)fsa.cSectorUnit * fsa.cbSector * fsa.cUnitAvail);
                   // ^ fixed V0.9.0

    return arc;
}

/*
 *@@ doshQueryDiskFSType:
 *       copies the file-system type of the given disk object
 *       (HPFS, FAT, CDFS etc.) to pszBuf.
 *       Returns the DOS error code.
 *
 *@@changed V0.9.1 (99-12-12) [umoeller]: added cbBuf to prototype
 *@@changed V0.9.14 (2001-08-01) [umoeller]: fixed, this never respected cbBuf
 *@@changed V0.9.16 (2001-10-02) [umoeller]: added check for valid logical disk no
 *@@changed V1.0.10 (2012-06-14) [pr]: fix buffer overwrite @@fixes 1206
 */

APIRET doshQueryDiskFSType(ULONG ulLogicalDrive, // in:  1 for A:, 2 for B:, 3 for C:, ...
                           PSZ pszBuf,           // out: buffer for FS type
                           ULONG cbBuf)          // in: size of that buffer
{
    APIRET arc = NO_ERROR;
    CHAR szName[5];

    BYTE   fsqBuffer[sizeof(FSQBUFFER2) + (3 * CCHMAXPATH)] = {0};
    ULONG  cbBuffer   = sizeof(fsqBuffer);        // Buffer length)
    PFSQBUFFER2 pfsqBuffer = (PFSQBUFFER2)fsqBuffer;

    // compose "D:"-type string from logical drive letter
    if (ulLogicalDrive > 0 && ulLogicalDrive < 27)
    {
        szName[0] = ulLogicalDrive + 'A' - 1;
        szName[1] = ':';
        szName[2] = '\0';

        arc = DosQueryFSAttach(szName,          // logical drive of attached FS ("D:"-style)
                               0,               // ulOrdinal, ignored for FSAIL_QUERYNAME
                               FSAIL_QUERYNAME, // return name for a drive or device
                               pfsqBuffer,      // buffer for returned data
                               &cbBuffer);      // sizeof(*pfsqBuffer)

        if (arc == NO_ERROR)
        {
            if (pszBuf && cbBuf)
            {
                // The data for the last three fields in the FSQBUFFER2
                // structure are stored at the offset of fsqBuffer.szName.
                // Each data field following fsqBuffer.szName begins
                // immediately after the previous item.
                strncpy(pszBuf,
                        (CHAR*)(&pfsqBuffer->szName) + pfsqBuffer->cbName + 1,
                        cbBuf);         // V0.9.14 (2001-08-01) [umoeller]
                *(pszBuf + cbBuf - 1) = '\0';
            }
        }
    }
    else
        arc = ERROR_INVALID_PARAMETER; // V0.9.16 (2001-10-02) [umoeller]

    return arc;
}

/*
 *@@ doshQueryDiskLabel:
 *      this returns the label of a disk into
 *      *pszVolumeLabel, which must be 12 bytes
 *      in size.
 *
 *      This function was added because the Toolkit
 *      information for DosQueryFSInfo is only partly
 *      correct. On OS/2 2.x, that function does not
 *      take an FSINFO structure as input, but a VOLUMELABEL.
 *      On Warp, this does take an FSINFO.
 *
 *      DosSetFSInfo is even worse. See doshSetDiskLabel.
 *
 *      See http://zebra.asta.fh-weingarten.de/os2/Snippets/Bugi6787.HTML
 *      for details.
 *
 *@@added V0.9.0 [umoeller]
 *@@changed V0.9.11 (2001-04-22) [umoeller]: this copied even with errors, fixed
 */

APIRET doshQueryDiskLabel(ULONG ulLogicalDrive,         // in:  1 for A:, 2 for B:, 3 for C:, ...
                          PSZ pszVolumeLabel)           // out: volume label (must be 12 chars in size)
{
    APIRET      arc;

    #ifdef __OS2V2X__
        VOLUMELABEL FSInfoBuf;
    #else
        FSINFO      FSInfoBuf;
    #endif

    arc = DosQueryFSInfo(ulLogicalDrive,
                         FSIL_VOLSER,
                         &FSInfoBuf,
                         sizeof(FSInfoBuf)); // depends

    if (!arc)       // V0.9.11 (2001-04-22) [umoeller]
    {
        #ifdef __OS2V2X__
            strcpy(pszVolumeLabel, FSInfoBuf.szVolLabel);
        #else
            strcpy(pszVolumeLabel, FSInfoBuf.vol.szVolLabel);
        #endif
    }

    return arc;
}

/*
 *@@ doshSetDiskLabel:
 *      this sets the label of a disk.
 *
 *      This function was added because the Toolkit
 *      information for DosSetFSInfo is flat out wrong.
 *      That function does not take an FSINFO structure
 *      as input, but a VOLUMELABEL. As a result, using
 *      that function with the Toolkit's calling specs
 *      results in ERROR_LABEL_TOO_LONG always.
 *
 *      See http://zebra.asta.fh-weingarten.de/os2/Snippets/Bugi6787.HTML
 *      for details.
 *
 *@@added V0.9.0 [umoeller]
 */

APIRET doshSetDiskLabel(ULONG ulLogicalDrive,        // in:  1 for A:, 2 for B:, 3 for C:, ...
                        PSZ pszNewLabel)
{
    VOLUMELABEL FSInfoBuf;

    // check length; 11 chars plus null byte allowed
    FSInfoBuf.cch = (BYTE)strlen(pszNewLabel);
    if (FSInfoBuf.cch < sizeof(FSInfoBuf.szVolLabel))
    {
        strcpy(FSInfoBuf.szVolLabel, pszNewLabel);

        return DosSetFSInfo(ulLogicalDrive,
                            FSIL_VOLSER,
                            &FSInfoBuf,
                            sizeof(FSInfoBuf));
    }

    return ERROR_LABEL_TOO_LONG;
}

/*
 *@@category: Helpers\Control program helpers\File name parsing
 */

/* ******************************************************************
 *
 *   File name parsing
 *
 ********************************************************************/

/*
 *@@ doshGetDriveSpec:
 *      returns the drive specification in pcszFullFile,
 *      if any is present. This is useful for UNC support.
 *
 *      This returns:
 *
 *      --  NO_ERROR: drive spec was given, and the output
 *          fields have been set.
 *
 *      --  ERROR_INVALID_NAME: incorrect UNC syntax.
 *
 *      --  ERROR_INVALID_DRIVE: second char is ':', but
 *          drive letter is not in the range [A-Z].
 *
 *      --  ERROR_INVALID_PARAMETER: no drive spec given
 *          at all; apparently pcszFullFile is not fully
 *          qualified in the first place, or it is NULL,
 *          or its length is <= 2.
 *
 *@@added V0.9.16 (2001-10-25) [umoeller]
 */

APIRET doshGetDriveSpec(PCSZ pcszFullFile,      // in: fully q'fied file spec
                        PSZ pszDrive,           // out: drive spec ("C:" or "\\SERVER\RESOURCE"; ptr can be NULL)
                        PULONG pulDriveLen,     // out: length of drive spec (2 if local drive; ptr can be NULL)
                        PBOOL pfIsUNC)          // out: set to TRUE if UNC name, FALSE otherwise (ptr can be NULL)
{
    APIRET  arc = NO_ERROR;
    ULONG   ulFileSpecLength;

    if (    (pcszFullFile)
         && (ulFileSpecLength = strlen(pcszFullFile))
         && (ulFileSpecLength >= 2)
       )
    {
        // upper-case the drive letter
        if (pcszFullFile[1] == ':')
        {
            CHAR cDrive = toupper(*pcszFullFile);
            // local drive specified:
            if (    (cDrive >= 'A')
                 && (cDrive <= 'Z')
               )
            {
                if (pszDrive)
                {
                    pszDrive[0] = cDrive;
                    pszDrive[1] = ':';
                    pszDrive[2] = '\0';
                }

                if (pulDriveLen)
                    *pulDriveLen = 2;
                if (pfIsUNC)
                    *pfIsUNC = FALSE;
            }
            else
                // this is not a valid drive:
                arc = ERROR_INVALID_DRIVE;
        }
        else if (    (pcszFullFile[0] == '\\')
                  && (pcszFullFile[1] == '\\')
                )
        {
            // UNC drive specified:
            // this better be a full \\SERVER\RESOURCE string
            PCSZ pResource;
            if (pResource = strchr(pcszFullFile + 3, '\\'))
            {
                // we got at least \\SERVER\:
                ULONG ulLength;
                PCSZ p;

                // check if more stuff is coming
                if (p = strchr(pResource + 1, '\\'))
                {
                    // yes: copy server and resource excluding that backslash
                    if (p == pResource + 1)
                        // "\\SERVER\\" is invalid
                        arc = ERROR_INVALID_NAME;
                    else
                        // we got "\\SERVER\something\":
                        // drop the last backslash
                        ulLength = p - pcszFullFile;
                }
                else
                    // "\\SERVER\something" only:
                    ulLength = ulFileSpecLength;

                if (!arc)
                {
                    if (pszDrive)
                    {
                        memcpy(pszDrive,
                               pcszFullFile,
                               ulLength);
                        pszDrive[ulLength] = '\0';
                    }

                    if (pulDriveLen)
                        *pulDriveLen = ulLength;
                    if (pfIsUNC)
                        *pfIsUNC = TRUE;
                }
            }
            else
                // invalid UNC name:
                arc = ERROR_INVALID_NAME;
        }
        else
            // neither local, nor UNC:
            arc = ERROR_INVALID_PARAMETER;
    }
    else
        arc = ERROR_INVALID_PARAMETER;

    return arc;
}

/*
 *@@ doshGetExtension:
 *      finds the file name extension of pszFilename,
 *      which can be a file name only or a fully
 *      qualified filename.
 *
 *      This returns a pointer into pszFilename to
 *      the character after the last dot.
 *
 *      Returns NULL if not found (e.g. if the filename
 *      has no dot in it).
 *
 *      In the pathological case of a dot in the path
 *      but not in the filename itself (e.g.
 *      "C:\files.new\readme"), this correctly returns
 *      NULL.
 *
 *@@added V0.9.6 (2000-10-16) [umoeller]
 *@@changed V0.9.7 (2000-12-10) [umoeller]: fixed "F:filename.ext" case
 */

PSZ doshGetExtension(PCSZ pcszFilename)
{
    PSZ pReturn = NULL;

    if (pcszFilename)
    {
        // find filename
        PCSZ    p2,
                pStartOfName = NULL,
                pExtension = NULL;

        if (p2 = strrchr(pcszFilename + 2, '\\'))
                            // works on "C:\blah" or "\\unc\blah"
            pStartOfName = p2 + 1;
        else
        {
            // no backslash found:
            // maybe only a drive letter was specified:
            if (pcszFilename[1] == ':')
                // yes:
                pStartOfName = pcszFilename + 2;
            else
                // then this is not qualified at all...
                // use start of filename
                pStartOfName = (PSZ)pcszFilename;
        }

        // find last dot in filename
        if (pExtension = strrchr(pStartOfName, '.'))
            pReturn = (PSZ)pExtension + 1;
    }

    return pReturn;
}

/*
 *@@category: Helpers\Control program helpers\File management
 */

/* ******************************************************************
 *
 *   File helpers
 *
 ********************************************************************/

/*
 *@@ doshIsFileOnFAT:
 *      returns TRUE if pszFileName resides on
 *      a FAT drive. Note that pszFileName must
 *      be fully qualified (i.e. the drive letter
 *      must be the first character), or this will
 *      return garbage.
 *
 *@@changed WarpIN V1.0.20 (2010-06-05) [pr]: now compares FSD name correctly
 */

BOOL doshIsFileOnFAT(const char* pcszFileName)
{
    BOOL brc = FALSE;
    CHAR szName[5];

    APIRET arc;
    BYTE   fsqBuffer[sizeof(FSQBUFFER2) + (3 * CCHMAXPATH)] = {0};
    ULONG  cbBuffer   = sizeof(fsqBuffer);        // Buffer length)
    PFSQBUFFER2 pfsqBuffer = (PFSQBUFFER2)fsqBuffer;

    szName[0] = pcszFileName[0];    // copy drive letter
    szName[1] = ':';
    szName[2] = '\0';

    if (!(arc = DosQueryFSAttach(szName,          // logical drive of attached FS
                                 0,               // ulOrdinal, ignored for FSAIL_QUERYNAME
                                 FSAIL_QUERYNAME, // return data for a Drive or Device
                                 pfsqBuffer,      // returned data
                                 &cbBuffer)))     // returned data length
    {
        // The data for the last three fields in the FSQBUFFER2
        // structure are stored at the offset of fsqBuffer.szName.
        // Each data field following fsqBuffer.szName begins
        // immediately after the previous item.
        if (!strncmp((PSZ)&(pfsqBuffer->szName) + pfsqBuffer->cbName + 1,
                     "FAT",
                     pfsqBuffer->cbFSDName))
            brc = TRUE;
    }

    return brc;
}

/*
 *@@ doshQueryFileSize:
 *      returns the size of an already opened file
 *      or 0 upon errors.
 *      Use doshQueryPathSize to query the size of
 *      any file.
 *
 *@@changed V0.9.16 (2001-10-19) [umoeller]: now returning APIRET
 */

APIRET doshQueryFileSize(HFILE hFile,       // in: file handle
                         PULONG pulSize)    // out: file size (ptr can be NULL)
{
    APIRET arc;
    FILESTATUS3 fs3;
    if (!(arc = DosQueryFileInfo(hFile, FIL_STANDARD, &fs3, sizeof(fs3))))
        if (pulSize)
            *pulSize = fs3.cbFile;
    return arc;
}

/*
 *@@ doshQueryPathSize:
 *      returns the size of any file,
 *      or 0 if the file could not be
 *      found.
 *
 *      Use doshQueryFileSize instead to query the
 *      size if you have a HFILE.
 *
 *      Otherwise this returns:
 *
 *      --  ERROR_FILE_NOT_FOUND
 *      --  ERROR_PATH_NOT_FOUND
 *      --  ERROR_SHARING_VIOLATION
 *      --  ERROR_FILENAME_EXCED_RANGE
 *      --  ERROR_INVALID_PARAMETER
 *
 *@@changed V0.9.16 (2001-10-19) [umoeller]: now returning APIRET
 */

APIRET doshQueryPathSize(PCSZ pcszFile,         // in: filename
                         PULONG pulSize)        // out: file size (ptr can be NULL)
{
    APIRET arc;

    if (pcszFile)      // V0.9.16 (2001-12-08) [umoeller]
    {
        FILESTATUS3 fs3;
        if (!(arc = DosQueryPathInfo((PSZ)pcszFile, FIL_STANDARD, &fs3, sizeof(fs3))))
            if (pulSize)
                *pulSize = fs3.cbFile;
    }
    else
        arc = ERROR_INVALID_PARAMETER;

    return arc;
}

/*
 *@@ doshQueryPathAttr:
 *      returns the file attributes of pszFile,
 *      which can be fully qualified. The
 *      attributes will be stored in *pulAttr.
 *      pszFile can also specify a directory,
 *      although not all attributes make sense
 *      for directories.
 *
 *      fAttr can be:
 *      --  FILE_ARCHIVED
 *      --  FILE_READONLY
 *      --  FILE_SYSTEM
 *      --  FILE_HIDDEN
 *
 *      This returns the APIRET of DosQueryPathInfo.
 *      *pulAttr is only valid if NO_ERROR is
 *      returned.
 *
 *      Otherwise this returns:
 *
 *      --  ERROR_FILE_NOT_FOUND
 *      --  ERROR_PATH_NOT_FOUND
 *      --  ERROR_SHARING_VIOLATION
 *      --  ERROR_FILENAME_EXCED_RANGE
 *
 *@@added V0.9.0 [umoeller]
 */

APIRET doshQueryPathAttr(const char* pcszFile,      // in: file or directory name
                         PULONG pulAttr)            // out: attributes (ptr can be NULL)
{
    FILESTATUS3 fs3;
    APIRET arc;

    if (!(arc = DosQueryPathInfo((PSZ)pcszFile,
                                  FIL_STANDARD,
                                  &fs3,
                                  sizeof(fs3))))
    {
        if (pulAttr)
            *pulAttr = fs3.attrFile;
    }

    return arc;
}

/*
 *@@ doshSetPathAttr:
 *      sets the file attributes of pszFile,
 *      which can be fully qualified.
 *      pszFile can also specify a directory,
 *      although not all attributes make sense
 *      for directories.
 *
 *      fAttr can be:
 *      --  FILE_ARCHIVED
 *      --  FILE_READONLY
 *      --  FILE_SYSTEM
 *      --  FILE_HIDDEN
 *
 *      Note that this simply sets all the given
 *      attributes; the existing attributes
 *      are lost.
 *
 *      This returns the APIRET of DosQueryPathInfo.
 */

APIRET doshSetPathAttr(const char* pcszFile,    // in: file or directory name
                       ULONG ulAttr)            // in: new attributes
{
    APIRET arc;

    if (pcszFile)
    {
        FILESTATUS3 fs3;
        if (!(arc = DosQueryPathInfo((PSZ)pcszFile,
                                      FIL_STANDARD,
                                      &fs3,
                                      sizeof(fs3))))
        {
            fs3.attrFile = ulAttr;
            arc = DosSetPathInfo((PSZ)pcszFile,
                                 FIL_STANDARD,
                                 &fs3,
                                 sizeof(fs3),
                                 DSPI_WRTTHRU);
        }
    }
    else
        arc = ERROR_INVALID_PARAMETER;

    return arc;
}

/*
 *@@ FindEAValue:
 *      returns the pointer to the EA value
 *      if the EA with the given name exists
 *      in the given FEA2LIST.
 *
 *      Within the FEA structure
 *
 +          typedef struct _FEA2 {
 +              ULONG      oNextEntryOffset;  // Offset to next entry.
 +              BYTE       fEA;               // Extended attributes flag.
 +              BYTE       cbName;            // Length of szName, not including NULL.
 +              USHORT     cbValue;           // Value length.
 +              CHAR       szName[1];         // Extended attribute name.
 +          } FEA2;
 *
 *      the EA value starts right after szName (plus its null
 *      terminator). The first USHORT of the value should
 *      normally signify the type of the EA, e.g. EAT_ASCII.
 *      This returns a pointer to that type USHORT.
 *
 *@@added V0.9.16 (2001-10-25) [umoeller]
 *@@changed V1.0.1 (2002-12-08) [umoeller]: moved this here from XWorkplace code, renamed from fsysFindEAValue
 */

PBYTE doshFindEAValue(PFEA2LIST pFEA2List2,      // in: file EA list
                      PCSZ pcszEAName,           // in: EA name to search for (e.g. ".LONGNAME")
                      PUSHORT pcbValue)          // out: length of value (ptr can be NULL)
{
    ULONG ulEANameLen;

    /*
    typedef struct _FEA2LIST {
        ULONG     cbList;   // Total bytes of structure including full list.
                            // Apparently, if EAs aren't supported, this
                            // is == sizeof(ULONG).
        FEA2      list[1];  // Variable-length FEA2 structures.
    } FEA2LIST;

    typedef struct _FEA2 {
        ULONG      oNextEntryOffset;  // Offset to next entry.
        BYTE       fEA;               // Extended attributes flag.
        BYTE       cbName;            // Length of szName, not including NULL.
        USHORT     cbValue;           // Value length.
        CHAR       szName[1];         // Extended attribute name.
    } FEA2;
    */

    if (!pFEA2List2)
        return NULL;

    if (    (pFEA2List2->cbList > sizeof(ULONG))
                    // FAT32 and CDFS return 4 for anything here, so
                    // we better not mess with anything else; I assume
                    // any FS which doesn't support EAs will do so then
         && (pcszEAName)
         && (ulEANameLen = strlen(pcszEAName))
       )
    {
        PFEA2 pThis = &pFEA2List2->list[0];
        // maintain a current offset so we will never
        // go beyond the end of the buffer accidentally...
        // who knows what these stupid EA routines return!
        ULONG ulOfsThis = sizeof(ULONG),
              ul = 0;

        while (ulOfsThis < pFEA2List2->cbList)
        {
            if (    (ulEANameLen == pThis->cbName)
                 && (!memcmp(pThis->szName,
                             pcszEAName,
                             ulEANameLen))
               )
            {
                if (pThis->cbValue)
                {
                    PBYTE pbValue =   (PBYTE)pThis
                                    + sizeof(FEA2)
                                    + pThis->cbName;
                    if (pcbValue)
                        *pcbValue = pThis->cbValue;
                    return pbValue;
                }
                else
                    // no value:
                    return NULL;
            }

            if (!pThis->oNextEntryOffset)
                // this was the last entry:
                return NULL;

            ulOfsThis += pThis->oNextEntryOffset;

            pThis = (PFEA2)(((PBYTE)pThis) + pThis->oNextEntryOffset);
            ul++;
        } // end while
    } // end if (    (pFEA2List2->cbList > sizeof(ULONG)) ...

    return NULL;
}

/*
 *@@ doshQueryLongname:
 *      attempts to find the value of the .LONGNAME EA in the
 *      given FEALIST and stores it in the pszLongname buffer,
 *      which must be CCHMAXPATH in size.
 *
 *      Returns TRUE if a .LONGNAME was found and copied.
 *
 *@@added V0.9.16 (2001-10-25) [umoeller]
 *@@changed V1.0.1 (2002-12-08) [umoeller]: moved this here from XWorkplace code, renamed from DecodeLongname
 */

BOOL doshQueryLongname(PFEA2LIST pFEA2List2,
                       PSZ pszLongname,          // out: .LONGNAME if TRUE is returned
                       PULONG pulNameLen)        // out: length of .LONGNAME string
{
    PBYTE pbValue;
    if (pbValue = doshFindEAValue(pFEA2List2,
                                  ".LONGNAME",
                                  NULL))
    {
        PUSHORT pusType = (PUSHORT)pbValue;
        if (*pusType == EAT_ASCII)
        {
            // CPREF: first word after EAT_ASCII specifies length
            PUSHORT pusStringLength = pusType + 1;      // pbValue + 2
            if (*pusStringLength)
            {
                ULONG cb = _min(*pusStringLength, CCHMAXPATH - 1);
                memcpy(pszLongname,
                       pbValue + 4,
                       cb);
                pszLongname[cb] = '\0';
                *pulNameLen = cb;
                return TRUE;
            }
        }
    }

    return FALSE;
}

/*
 *@@category: Helpers\Control program helpers\File management\XFILEs
 */

/* ******************************************************************
 *
 *   XFILEs
 *
 ********************************************************************/

/*
 * doshOpen:
 *      wrapper around DosOpen for simpler opening
 *      of files.
 *
 *      ulOpenMode determines the mode to open the
 *      file in (fptr specifies the position after
 *      the open):
 *
 +      +-------------------------+------+------------+-----------+
 +      |  ulOpenMode             | mode | if exists  | if new    |
 +      +-------------------------+------+------------+-----------+
 +      |  XOPEN_READ_EXISTING    | read | opens      | fails     |
 +      |                         |      | fptr = 0   |           |
 +      +-------------------------+------+------------+-----------+
 +      |  XOPEN_READWRITE_EXISTING r/w  | opens      | fails     |
 +      |                         |      | fptr = 0   |           |
 +      +-------------------------+------+------------+-----------+
 +      |  XOPEN_READWRITE_APPEND | r/w  | opens,     | creates   |
 +      |                         |      | appends    |           |
 +      |                         |      | fptr = end | fptr = 0  |
 +      +-------------------------+------+------------+-----------+
 +      |  XOPEN_READWRITE_NEW    | r/w  | replaces   | creates   |
 +      |                         |      | fptr = 0   | fptr = 0  |
 +      +-------------------------+------+------------+-----------+
 *
 *      In addition, you can OR one of the above values with
 *      the XOPEN_BINARY flag:
 *
 *      --  If XOPEN_BINARY is set, no conversion is performed
 *          on read and write.
 *
 *      --  If XOPEN_BINARY is _not_ set, all \n chars are
 *          converted to \r\n on write.
 *
 *      *ppFile receives a new XFILE structure describing
 *      the open file, if NO_ERROR is returned.
 *
 *      The file pointer is then set to the beginning of the
 *      file _unless_ XOPEN_READWRITE_APPEND was specified;
 *      in that case only, the file pointer is set to the
 *      end of the file so data can be appended (see above).
 *
 *      Otherwise this returns:
 *
 *      --  ERROR_FILE_NOT_FOUND
 *      --  ERROR_PATH_NOT_FOUND
 *      --  ERROR_SHARING_VIOLATION
 *      --  ERROR_FILENAME_EXCED_RANGE
 *
 *      --  ERROR_NOT_ENOUGH_MEMORY
 *      --  ERROR_INVALID_PARAMETER
 *
 *@@added V0.9.16 (2001-10-19) [umoeller]
 *@@changed V0.9.16 (2001-12-18) [umoeller]: fixed error codes
 *@@changed V1.0.1 (2003-01-10) [umoeller]: now allowing read for all modes
 *@@changed V1.0.2 (2003-11-13) [umoeller]: optimized; now calling doshQueryPathSize only on failure
 */

APIRET doshOpen(PCSZ pcszFilename,   // in: filename to open
                ULONG flOpenMode,       // in: XOPEN_* mode
                PULONG pcbFile,         // in: new file size (if new file is created)
                                        // out: file size
                PXFILE *ppFile)
{
    APIRET  arc = NO_ERROR;

    ULONG   fsOpenFlags = 0,
            fsOpenMode =    OPEN_FLAGS_FAIL_ON_ERROR
                          | OPEN_FLAGS_NO_LOCALITY
                          | OPEN_FLAGS_NOINHERIT;

    switch (flOpenMode & XOPEN_ACCESS_MASK)
    {
        case XOPEN_READ_EXISTING:
            fsOpenFlags =   OPEN_ACTION_FAIL_IF_NEW
                          | OPEN_ACTION_OPEN_IF_EXISTS;
            fsOpenMode |=   OPEN_SHARE_DENYWRITE
                          | OPEN_ACCESS_READONLY;

            // run this first, because if the file doesn't
            // exists, DosOpen only returns ERROR_OPEN_FAILED,
            // which isn't that meaningful
            // V0.9.16 (2001-12-08) [umoeller]
            /* arc = doshQueryPathSize(pcszFilename,
                                    pcbFile); */
                // moved this down V1.0.2 (2003-11-13) [umoeller]
        break;

        case XOPEN_READWRITE_EXISTING:
            fsOpenFlags =   OPEN_ACTION_FAIL_IF_NEW
                          | OPEN_ACTION_OPEN_IF_EXISTS;
            fsOpenMode |=   OPEN_SHARE_DENYWRITE
                          | OPEN_ACCESS_READWRITE;

            /* arc = doshQueryPathSize(pcszFilename,
                                    pcbFile); */
                // moved this down V1.0.2 (2003-11-13) [umoeller]
        break;

        case XOPEN_READWRITE_APPEND:
            fsOpenFlags =   OPEN_ACTION_CREATE_IF_NEW
                          | OPEN_ACTION_OPEN_IF_EXISTS;
            fsOpenMode |=   OPEN_SHARE_DENYWRITE // OPEN_SHARE_DENYREADWRITE V1.0.1 (2003-01-10) [umoeller]
                          | OPEN_ACCESS_READWRITE;
            // _Pmpf((__FUNCTION__ ": opening XOPEN_READWRITE_APPEND"));
        break;

        case XOPEN_READWRITE_NEW:
            fsOpenFlags =   OPEN_ACTION_CREATE_IF_NEW
                          | OPEN_ACTION_REPLACE_IF_EXISTS;
            fsOpenMode |=   OPEN_SHARE_DENYWRITE // OPEN_SHARE_DENYREADWRITE V1.0.1 (2003-01-10) [umoeller]
                          | OPEN_ACCESS_READWRITE;
            // _Pmpf((__FUNCTION__ ": opening XOPEN_READWRITE_NEW"));
        break;
    }

    if (    (!arc)
         && fsOpenFlags
         && pcbFile
         && ppFile
       )
    {
        PXFILE pFile;
        if (pFile = NEW(XFILE))
        {
            ULONG ulAction;

            ZERO(pFile);

            // copy open flags
            pFile->flOpenMode = flOpenMode;

            if (!(arc = DosOpen((PSZ)pcszFilename,
                                &pFile->hf,
                                &ulAction,
                                *pcbFile,
                                FILE_ARCHIVED,
                                fsOpenFlags,
                                fsOpenMode,
                                NULL)))       // EAs
            {
                // alright, got the file:

                if (    (ulAction == FILE_EXISTED)
                     && ((flOpenMode & XOPEN_ACCESS_MASK) == XOPEN_READWRITE_APPEND)
                   )
                    // get its size and set ptr to end for append
                    arc = DosSetFilePtr(pFile->hf,
                                        0,
                                        FILE_END,
                                        pcbFile);
                else
                    arc = doshQueryFileSize(pFile->hf,
                                            pcbFile);
                    // file ptr is at beginning

                #ifdef DEBUG_DOSOPEN
                 if (arc)
                    _Pmpf((__FUNCTION__ ": DosSetFilePtr/queryfilesize returned %d for %s",
                                arc, pcszFilename));
                #endif

                // store file size
                pFile->cbInitial
                = pFile->cbCurrent
                = *pcbFile;

                pFile->pszFilename = strdup(pcszFilename);
            }
            else
            {
                #ifdef DEBUG_DOSOPEN
                    _Pmpf((__FUNCTION__ ": DosOpen returned %d for %s",
                             arc, pcszFilename));
                #endif

                // open failed: if the file doesn't exist, DosOpen only
                // returns OPEN_FAILED, while ERROR_FILE_NOT_FOUND would
                // be a bit more informative
                // (this check used to be before DosOpen, but is a bit
                // excessive and should only be run if we really have no open)
                if (arc == ERROR_OPEN_FAILED)
                    arc = doshQueryPathSize(pcszFilename,
                                            pcbFile);
            }

            if (arc)
                doshClose(&pFile);
            else
                *ppFile = pFile;
        }
        else
            arc = ERROR_NOT_ENOUGH_MEMORY;
    }
    else
        if (!arc)       // V0.9.19 (2002-04-02) [umoeller]
            arc = ERROR_INVALID_PARAMETER;

    return arc;
}

/*
 *@@ doshReadAt:
 *      reads cb bytes from the position specified by
 *      lOffset into the buffer pointed to by pbData,
 *      which should be cb bytes in size.
 *
 *      Note that lOffset is always considered to
 *      be from the beginning of the file (FILE_BEGIN
 *      method).
 *
 *      This implements a small cache so that several
 *      calls with a near offset will not touch the
 *      disk always. The cache has been optimized for
 *      the exeh* functions and works quite nicely
 *      there.
 *
 *      Note that the position of the file pointer is
 *      undefined after calling this function because
 *      the data might have been returned from the
 *      cache.
 *
 *      fl may be any combination of the following:
 *
 *      --  DRFL_NOCACHE: do not fill the cache with
 *          new data if the data is not in the cache
 *          currently.
 *
 *      --  DRFL_FAILIFLESS: return ERROR_NO_DATA
 *          if the data returned by DosRead is less
 *          than what was specified. This might
 *          simplify error handling.
 *
 *@@added V0.9.13 (2001-06-14) [umoeller]
 *@@changed V0.9.16 (2001-12-18) [umoeller]: now with XFILE, and always using FILE_BEGIN
 *@@changed V0.9.19 (2002-04-02) [umoeller]: added params checking
 *@@changed V1.0.2 (2003-11-13) [umoeller]: optimized cache (using realloc)
 */

APIRET doshReadAt(PXFILE pFile,
                  ULONG ulOffset,   // in: offset to read from (from beginning of file)
                  PULONG pcb,       // in: bytes to read, out: bytes read (req.)
                  PBYTE pbData,     // out: read buffer (must be cb bytes)
                  ULONG fl)         // in: DRFL_* flags
{
    APIRET arc = NO_ERROR;
    ULONG cb;
    ULONG ulDummy;

    if (!pFile || !pcb)
        // V0.9.19 (2002-04-02) [umoeller]
        return ERROR_INVALID_PARAMETER;

    cb = *pcb;
    *pcb = 0;

    // check if we have the data in the cache already;

    if (    (pFile->pbCache)
            // first byte must be in cache
         && (ulOffset >= pFile->ulReadFrom)
            // last byte must be in cache
         && (    ulOffset + cb
              <= pFile->ulReadFrom + pFile->cbCache
            )
       )
    {
        // alright, return data from cache simply
        ULONG ulOfsInCache = ulOffset - pFile->ulReadFrom;

        memcpy(pbData,
               pFile->pbCache + ulOfsInCache,
               cb);
        *pcb = cb;

        #ifdef DEBUG_DOSOPEN
        _Pmpf((__FUNCTION__ " %s: data is fully in cache",
                    pFile->pszFilename));
        _Pmpf(("  caller wants %d bytes from %d",
                    cb, ulOffset));
        _Pmpf(("  we got %d bytes from %d",
                    pFile->cbCache, pFile->ulReadFrom));
        _Pmpf(("  so copied %d bytes from cache ofs %d",
                    cb, ulOfsInCache));
        #endif
    }
    else
    {
        // data is not in cache:
        // check how much it is... for small amounts,
        // we load the cache first
        if (    (cb <= 4096 - 512)
             && (!(fl & DRFL_NOCACHE))
           )
        {
            #ifdef DEBUG_DOSOPEN
            _Pmpf((__FUNCTION__ " %s: filling cache anew",
                    pFile->pszFilename));
            _Pmpf(("  caller wants %d bytes from %d",
                        cb, ulOffset));
            #endif

            // OK, then fix the offset to read from
            // to a multiple of 512 to get a full sector
            pFile->ulReadFrom = ulOffset / 512L * 512L;
            // and read 4096 bytes always plus the
            // value we cut off above
            pFile->cbCache = 4096;

            #ifdef DEBUG_DOSOPEN
            _Pmpf(("  getting %d bytes from %d",
                        pFile->cbCache, pFile->ulReadFrom));
            #endif

#if 0
            // free old cache
            if (pFile->pbCache)
                free(pFile->pbCache);

            // allocate new cache
            if (!(pFile->pbCache = (PBYTE)malloc(pFile->cbCache)))
#else
            // realloc is better V1.0.2 (2003-11-13) [umoeller]
            if (!(pFile->pbCache = (PBYTE)realloc(pFile->pbCache,
                                                  pFile->cbCache)))
#endif
                arc = ERROR_NOT_ENOUGH_MEMORY;
            else
            {
                ULONG ulOfsInCache = 0;

                if (!(arc = DosSetFilePtr(pFile->hf,
                                          (LONG)pFile->ulReadFrom,
                                          FILE_BEGIN,
                                          &ulDummy)))
                {
                    if (!(arc = DosRead(pFile->hf,
                                        pFile->pbCache,
                                        pFile->cbCache,
                                        &ulDummy)))
                    {
                        // got data:
                        #ifdef DEBUG_DOSOPEN
                        _Pmpf(("        %d bytes read", ulDummy));
                        #endif

                        pFile->cbCache = ulDummy;

                        // check bounds
                        ulOfsInCache = ulOffset - pFile->ulReadFrom;

                        /*
                        if (ulOfsInCache + cb > pFile->cbCache)
                        {
                            cb = pFile->cbCache - ulOfsInCache;
                            if (fl & DRFL_FAILIFLESS)
                                arc = ERROR_NO_DATA;
                        }
                        */
                    }
                }

                if (!arc)
                {
                    // copy to caller
                    memcpy(pbData,
                           pFile->pbCache + ulOfsInCache,
                           cb);
                    *pcb = cb;

                    #ifdef DEBUG_DOSOPEN
                    _Pmpf(("  so copied %d bytes from cache ofs %d",
                                cb, ulOfsInCache));
                    #endif
                }
                else
                {
                    free(pFile->pbCache);
                    pFile->pbCache = NULL;
                }
            } // end else if (!(pFile->pbCache = (PBYTE)malloc(pFile->cbCache)))
        }
        else
        {
            // read uncached:
            #ifdef DEBUG_DOSOPEN
            _Pmpf(("  " __FUNCTION__ " %s: reading uncached",
                        pFile->pszFilename));
            _Pmpf(("      caller wants %d bytes from %d",
                        cb, ulOffset));
            #endif

            if (!(arc = DosSetFilePtr(pFile->hf,
                                      (LONG)ulOffset,
                                      FILE_BEGIN,
                                      &ulDummy)))
            {
                if (!(arc = DosRead(pFile->hf,
                                    pbData,
                                    cb,
                                    &ulDummy)))
                {
                    if (    (fl & DRFL_FAILIFLESS)
                         && (ulDummy != cb)
                       )
                        arc = ERROR_NO_DATA;
                    else
                        *pcb = ulDummy;     // bytes read
                }
            }
        }
    }

    return arc;
}

/*
 *@@ doshWrite:
 *      writes the specified data to the file.
 *      If (cb == 0), this runs strlen on pcsz
 *      to find out the length.
 *
 *      If the file is not in binary mode, all
 *      \n chars are converted to \r\n before
 *      writing.
 *
 *      Note that this expects that the file
 *      pointer is at the end of the file, or
 *      you will get garbage.
 *
 *@@added V0.9.16 (2001-10-19) [umoeller]
 *@@changed V0.9.16 (2001-12-02) [umoeller]: added XOPEN_BINARY \r\n support
 *@@changed V0.9.16 (2001-12-06) [umoeller]: added check for pFile != NULL
 *@@changed V1.0.1 (2003-01-10) [umoeller]: now forcing DosSetFileSize
 */

APIRET doshWrite(PXFILE pFile,
                 ULONG cb,
                 PCSZ pbData)
{
    APIRET arc = NO_ERROR;
    if ((!pFile) || (!pbData))
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        if (!cb)
            cb = strlen(pbData);

        if (!cb)
            arc = ERROR_INVALID_PARAMETER;
        else
        {
            PSZ pszNew = NULL;

            if (!(pFile->flOpenMode & XOPEN_BINARY))
            {
                // convert all \n to \r\n:
                // V0.9.16 (2001-12-02) [umoeller]

                // count all \n first
                ULONG cNewLines = 0;
                PCSZ pSource = pbData;
                ULONG ul;
                for (ul = 0;
                     ul < cb;
                     ul++)
                {
                    if (*pSource++ == '\n')
                        cNewLines++;
                }

                if (cNewLines)
                {
                    // we have '\n' chars:
                    // then we need just as many \r chars inserted
                    ULONG cbNew = cb + cNewLines;
                    if (!(pszNew = (PSZ)malloc(cbNew)))
                        arc = ERROR_NOT_ENOUGH_MEMORY;
                    else
                    {
                        PSZ pTarget = pszNew;
                        pSource = pbData;
                        for (ul = 0;
                             ul < cb;
                             ul++)
                        {
                            CHAR c = *pSource++;
                            if (c == '\n')
                                *pTarget++ = '\r';
                            *pTarget++ = c;
                        }

                        cb = cbNew;
                    }
                }
            }

            if (!arc)
            {
                ULONG cbWritten;
                if (!(arc = DosWrite(pFile->hf,
                                     (pszNew)
                                            ? pszNew
                                            : (PSZ)pbData,
                                     cb,
                                     &cbWritten)))
                {
                    pFile->cbCurrent += cbWritten;
                    // invalidate the cache
                    FREE(pFile->pbCache);

                    // force a writeout V1.0.1 (2003-01-10) [umoeller]
                    DosSetFileSize(pFile->hf, pFile->cbCurrent);
                }
            }

            if (pszNew)
                free(pszNew);
        }
    }

    return arc;
}

/*
 *@@ doshWriteAt:
 *      writes cb bytes (pointed to by pbData) to the
 *      specified file at the position lOffset (from
 *      the beginning of the file).
 *
 *@@added V0.9.13 (2001-06-14) [umoeller]
 */

APIRET doshWriteAt(PXFILE pFile,
                   ULONG ulOffset,    // in: offset to write at
                   ULONG cb,        // in: bytes to write
                   PCSZ pbData)     // in: ptr to bytes to write (must be cb bytes)
{
    APIRET arc = NO_ERROR;
    ULONG cbWritten;
    if (!(arc = DosSetFilePtr(pFile->hf,
                              (LONG)ulOffset,
                              FILE_BEGIN,
                              &cbWritten)))
    {
        if (!(arc = DosWrite(pFile->hf,
                             (PSZ)pbData,
                             cb,
                             &cbWritten)))
        {
            if (ulOffset + cbWritten > pFile->cbCurrent)
                pFile->cbCurrent = ulOffset + cbWritten;
            // invalidate the cache V0.9.19 (2002-04-02) [umoeller]
            FREE(pFile->pbCache);
        }
    }

    return arc;
}

/*
 *@@ doshWriteLogEntry
 *      writes a log string to an XFILE, adding a
 *      leading timestamp before the line.
 *
 *      The internal string buffer is limited to 2000
 *      characters. Length checking is _not_ performed.
 *
 *@@added V0.9.16 (2001-10-19) [umoeller]
 *@@changed V0.9.16 (2001-12-06) [umoeller]: added check for pFile != NULL
 */

APIRET doshWriteLogEntry(PXFILE pFile,
                         const char* pcszFormat,
                         ...)
{
    APIRET arc = NO_ERROR;

    if ((!pFile) || (!pcszFormat))
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        DATETIME dt;
        CHAR szTemp[2000];
        ULONG   ulLength;

        DosGetDateTime(&dt);
        if (ulLength = sprintf(szTemp,
                               "%04d-%02d-%02d %02d:%02d:%02d:%02d ",
                               dt.year, dt.month, dt.day,
                               dt.hours, dt.minutes, dt.seconds, dt.hundredths))
        {
            if (!(arc = doshWrite(pFile,
                                  ulLength,
                                  szTemp)))
            {
                va_list arg_ptr;
                va_start(arg_ptr, pcszFormat);
                ulLength = vsprintf(szTemp, pcszFormat, arg_ptr);
                va_end(arg_ptr);

                if (pFile->flOpenMode & XOPEN_BINARY)
                    // if we're in binary mode, we need to add \r too
                    szTemp[ulLength++] = '\r';
                szTemp[ulLength++] = '\n';

                arc = doshWrite(pFile,
                                ulLength,
                                szTemp);
            }
        }
    }

    return arc;
}

/*
 * doshClose:
 *      closes an XFILE opened by doshOpen and
 *      sets *ppFile to NULL.
 *
 *@@added V0.9.16 (2001-10-19) [umoeller]
 */

APIRET doshClose(PXFILE *ppFile)
{
    APIRET arc = NO_ERROR;
    PXFILE pFile;

    if (    (ppFile)
         && (pFile = *ppFile)
       )
    {
        // set the ptr to NULL
        *ppFile = NULL;

        FREE(pFile->pbCache);
        FREE(pFile->pszFilename);

        if (pFile->hf)
        {
            DosSetFileSize(pFile->hf, pFile->cbCurrent);
            DosClose(pFile->hf);
            pFile->hf = NULLHANDLE;
        }

        free(pFile);
    }
    else
        arc = ERROR_INVALID_PARAMETER;

    return arc;
}

/*
 *@@ doshReadText:
 *      reads all the contents of the given XFILE into
 *      a newly allocated buffer. Handles Ctrl-Z properly.
 *
 *      Implementation for doshLoadTextFile, but can
 *      be called separately now.
 *
 *@@added V0.9.20 (2002-07-19) [umoeller]
 */

APIRET doshReadText(PXFILE pFile,
                    PSZ* ppszContent,   // out: newly allocated buffer with file's content
                    PULONG pcbRead)     // out: size of that buffer including null byte (ptr can be NULL)
{
    APIRET  arc;
    PSZ     pszContent;

    if (!(pszContent = (PSZ)malloc(pFile->cbCurrent + 1)))
        arc = ERROR_NOT_ENOUGH_MEMORY;
    else
    {
        ULONG cbRead = 0;
        if (!(arc = DosRead(pFile->hf,
                            pszContent,
                            pFile->cbCurrent,
                            &cbRead)))
        {
            if (cbRead != pFile->cbCurrent)
                arc = ERROR_NO_DATA;
            else
            {
                PSZ p;
                pszContent[cbRead] = '\0';

                // check if we have a ctrl-z (EOF) marker
                // this is present, for example, in config.sys
                // after install, and stupid E.EXE writes this
                // all the time when saving a file
                // V0.9.18 (2002-03-08) [umoeller]
                // if (p = strchr(pszContent, '\26')) What the hell is this??? Octal 26 = Decimal 22 != Ctrl-Z
                if (p = strchr(pszContent, '\x1A')) // V1.0.4 (2004-12-18) [pr]
                {
                    *p = '\0';
                    cbRead = p - pszContent;
                }

                *ppszContent = pszContent;
                if (pcbRead)
                    *pcbRead = cbRead + 1;
            }
        }

        if (arc)
            free(pszContent);
    }

    return arc;
}

/*
 *@@ doshLoadTextFile:
 *      reads a text file from disk, allocates memory
 *      via malloc() and sets a pointer to this
 *      buffer (or NULL upon errors).
 *
 *      This allocates one extra byte to make the
 *      buffer null-terminated always. The buffer
 *      is _not_ converted WRT the line format.
 *
 *      If CTRL-Z (ASCII 26) is encountered in the
 *      content, it is set to the null character
 *      instead (V0.9.18).
 *
 *      This returns the APIRET of DosOpen and DosRead.
 *      If any error occurred, no buffer was allocated.
 *      Otherwise, you should free() the buffer when
 *      no longer needed.
 *
 *@@changed V0.9.7 (2001-01-15) [umoeller]: renamed from doshReadTextFile
 *@@changed V0.9.16 (2002-01-05) [umoeller]: added pcbRead
 *@@changed V0.9.16 (2002-01-05) [umoeller]: rewritten using doshOpen
 *@@changed V0.9.18 (2002-03-08) [umoeller]: fixed ctrl-z (EOF) bug
 */

APIRET doshLoadTextFile(PCSZ pcszFile,      // in: file name to read
                        PSZ* ppszContent,   // out: newly allocated buffer with file's content
                        PULONG pcbRead)     // out: size of that buffer including null byte (ptr can be NULL)
{
    APIRET  arc;

    ULONG   cbFile = 0;
    PXFILE  pFile = NULL;

    if (!(arc = doshOpen(pcszFile,
                         XOPEN_READ_EXISTING,
                         &cbFile,
                         &pFile)))
    {
        doshReadText(pFile,
                     ppszContent,
                     pcbRead);
        doshClose(&pFile);
    }

    return arc;
}

/*
 *@@ doshCreateLogFilename:
 *      produces a log filename in pszBuf.
 *      If $(LOGFILES) is set, that directory
 *      is used; otherwise we use the root
 *      directory of the boot drive.
 *
 *@@added V1.0.1 (2003-01-25) [umoeller]
 */

BOOL doshCreateLogFilename(PSZ pszBuf,             // out: fully qualified filename
                           PCSZ pcszFilename,      // in: short log filename
                           BOOL fAllowBootDrive)   // in: allow creating log files on boot drive?
{
    CHAR    szBoot[] = "?:";
#if __cplusplus             // stupid OS/2 C++ prototypes
    PCSZ     pszLogDir;
#else
    PSZ     pszLogDir;
#endif
    if (DosScanEnv("LOGFILES",      // new eCS 1.1 setting
                   &pszLogDir))
    {
        // variable not set:
        if (!fAllowBootDrive)
            return FALSE;

        szBoot[0] = doshQueryBootDrive();
        pszLogDir = szBoot;
    }

    sprintf(pszBuf,
            "%s\\%s",
            pszLogDir,
            pcszFilename);

    return TRUE;
}

/*
 *@@ doshCreateBackupFileName:
 *      creates a valid backup filename of pszExisting
 *      with a numerical file name extension which does
 *      not exist in the directory where pszExisting
 *      resides.
 *      Returns a PSZ to a new buffer which was allocated
 *      using malloc().
 *
 *      <B>Example:</B> returns "C:\CONFIG.002" for input
 *      "C:\CONFIG.SYS" if "C:\CONFIG.001" already exists.
 *
 *@@changed V0.9.1 (99-12-13) [umoeller]: this crashed if pszExisting had no file extension
 */

PSZ doshCreateBackupFileName(const char* pszExisting)
{
    CHAR    szFilename[CCHMAXPATH];
    PSZ     pszLastDot;
    ULONG   ulCount = 1;
    CHAR    szCount[5];
    ULONG   ulDummy;

    strcpy(szFilename, pszExisting);

    if (!(pszLastDot = strrchr(szFilename, '.')))
        // no dot in filename:
        pszLastDot = szFilename + strlen(szFilename);

    do
    {
        sprintf(szCount, ".%03lu", ulCount);
        strcpy(pszLastDot, szCount);
        ulCount++;
    } while (!doshQueryPathSize(szFilename, &ulDummy));

    return strdup(szFilename);
}

/*
 *@@ doshCreateTempFileName:
 *      produces a file name in the the specified directory
 *      or $(TEMP) which presently doesn't exist. This
 *      checks the directory for existing files, but does
 *      not open the temp file.
 *
 *      If (pcszDir != NULL), we look into that directory.
 *      Otherwise we look into the directory specified
 *      by the $(TEMP) environment variable.
 *      If $(TEMP) is not set, $(TMP) is tried next.
 *
 *      If the directory thus specified does not exist, the
 *      root directory of the boot drive is used instead.
 *      As a result, this function should be fairly bomb-proof.
 *
 *      If (pcszExt != NULL), the temp file receives
 *      that extension, or no extension otherwise.
 *      Do not specify the dot in pcszExt.
 *
 *      pszTempFileName receives the fully qualified
 *      file name of the temp file in that directory
 *      and must point to a buffer CCHMAXPATH in size.
 *      The file name is 8+3 compliant if pcszExt does
 *      not exceed three characters.
 *
 *      If (pcszPrefix != NULL), the temp file name
 *      is prefixed with pcszPrefix. Since the temp
 *      file name must not exceed 8+3 letters, we
 *      can only use ( 8 - strlen(pcszPrefix ) digits
 *      for a random number to make the temp file name
 *      unique. You must therefore use a maximum of
 *      four characters for the prefix. Otherwise
 *      ERROR_INVALID_PARAMETER is returned.
 *
 *      Example: Assuming TEMP is set to C:\TEMP,
 +
 +          doshCreateTempFileName(szBuffer,
 +                                 NULL,             // use $(TEMP)
 +                                 "pre",            // prefix
 +                                 "tmp")            // extension
 +
 *      would produce something like "C:\TEMP\pre07FG2.tmp".
 *
 *@@added V0.9.9 (2001-04-04) [umoeller]
 */

APIRET doshCreateTempFileName(PSZ pszTempFileName,        // out: fully q'fied temp file name
                              PCSZ pcszDir,        // in: dir or NULL for %TEMP%
                              PCSZ pcszPrefix,     // in: prefix for temp file or NULL
                              PCSZ pcszExt)        // in: extension (without dot) or NULL
{
    APIRET      arc = NO_ERROR;

    ULONG       ulPrefixLen = (pcszPrefix)
                                    ? strlen(pcszPrefix)
                                    : 0;

    if (    (!pszTempFileName)
         || (ulPrefixLen > 4)
       )
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        CHAR        szDir[CCHMAXPATH] = "";
        FILESTATUS3 fs3;

        const char  *pcszTemp = pcszDir;

        if (!pcszTemp)
        {
            if (!(pcszTemp = getenv("TEMP")))
                pcszTemp = getenv("TMP");
        }

        if (pcszTemp)       // either pcszDir or $(TEMP) or $(TMP) now
            if (DosQueryPathInfo((PSZ)pcszTemp,
                                 FIL_STANDARD,
                                 &fs3,
                                 sizeof(fs3)))
                // TEMP doesn't exist:
                pcszTemp = NULL;

        if (!pcszTemp)
            // not set, or doesn't exist:
            // use root directory on boot drive
            sprintf(szDir,
                    "%c:\\",
                    doshQueryBootDrive());
        else
        {
            strcpy(szDir, pcszTemp);
            if (szDir[strlen(szDir) - 1] != '\\')
                strcat(szDir, "\\");
        }

        if (!szDir[0])
            arc = ERROR_PATH_NOT_FOUND;     // shouldn't happen
        else
        {
            ULONG       ulRandom = 0;
            ULONG       cAttempts = 0;

            // produce random number
            DosQuerySysInfo(QSV_MS_COUNT,
                            QSV_MS_COUNT,
                            &ulRandom,
                            sizeof(ulRandom));

            do
            {
                CHAR szFile[20] = "",
                     szFullTryThis[CCHMAXPATH];

                // use the lower eight hex digits of the
                // system uptime as the temp dir name
                sprintf(szFile,
                        "%08lX",
                        ulRandom & 0xFFFFFFFF);

                // if prefix is specified, overwrite the
                // first characters in the random number
                if (pcszPrefix)
                    memcpy(szFile, pcszPrefix, ulPrefixLen);

                if (pcszExt)
                {
                    szFile[8] = '.';
                    strcpy(szFile + 9, pcszExt);
                }

                // now compose full temp file name
                strcpy(szFullTryThis, szDir);
                strcat(szFullTryThis, szFile);
                // now we have: "C:\temp\wpiXXXXX"
                if (DosQueryPathInfo(szFullTryThis,
                                     FIL_STANDARD,
                                     &fs3,
                                     sizeof(fs3))
                        == ERROR_FILE_NOT_FOUND)
                {
                    // file or dir doesn't exist:
                    // cool, we're done
                    strcpy(pszTempFileName, szFullTryThis);
                    return NO_ERROR;
                }

                // if this didn't work, raise ulRandom and try again
                ulRandom += 123;

                // try only 100 times, just to be sure
                cAttempts++;
            } while (cAttempts < 100);

            // 100 loops elapsed:
            arc = ERROR_BAD_FORMAT;
        }
    }

    return arc;
}

/*
 *@@ doshWriteTextFile:
 *      writes a text file to disk; pszFile must contain the
 *      whole path and filename.
 *
 *      An existing file will be backed up if (pszBackup != NULL),
 *      using doshCreateBackupFileName. In that case, pszBackup
 *      receives the name of the backup created, so that buffer
 *      should be CCHMAXPATH in size.
 *
 *      If (pszbackup == NULL), an existing file will be overwritten.
 *
 *      On success (NO_ERROR returned), *pulWritten receives
 *      the no. of bytes written.
 *
 *@@changed V0.9.3 (2000-05-01) [umoeller]: optimized DosOpen; added error checking; changed prototype
 *@@changed V0.9.3 (2000-05-12) [umoeller]: added pszBackup
 */

APIRET doshWriteTextFile(const char* pszFile,        // in: file name
                         const char* pszContent,     // in: text to write
                         PULONG pulWritten,          // out: bytes written (ptr can be NULL)
                         PSZ pszBackup)              // in/out: create-backup?
{
    APIRET  arc = NO_ERROR;
    ULONG   ulWritten = 0;

    if ((!pszFile) || (!pszContent))
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        ULONG   ulAction = 0,
                ulLocal = 0;
        HFILE   hFile = 0;

        ULONG ulSize = strlen(pszContent);      // exclude 0 byte

        if (pszBackup)
        {
            PSZ     pszBackup2 = doshCreateBackupFileName(pszFile);
            DosCopy((PSZ)pszFile,
                    pszBackup2,
                    DCPY_EXISTING); // delete existing
            strcpy(pszBackup, pszBackup2);
            free(pszBackup2);
        }

        if (!(arc = DosOpen((PSZ)pszFile,
                            &hFile,
                            &ulAction,                      // action taken
                            ulSize,                         // primary allocation size
                            FILE_ARCHIVED | FILE_NORMAL,    // file attribute
                            OPEN_ACTION_CREATE_IF_NEW
                               | OPEN_ACTION_REPLACE_IF_EXISTS,  // open flags
                            OPEN_FLAGS_NOINHERIT
                               | OPEN_FLAGS_SEQUENTIAL         // sequential, not random access
                               | OPEN_SHARE_DENYWRITE          // deny write mode
                               | OPEN_ACCESS_WRITEONLY,        // write mode
                            NULL)))                         // no EAs
        {
            if (!(arc = DosSetFilePtr(hFile,
                                      0L,
                                      FILE_BEGIN,
                                      &ulLocal)))
                if (!(arc = DosWrite(hFile,
                                     (PVOID)pszContent,
                                     ulSize,
                                     &ulWritten)))
                    arc = DosSetFileSize(hFile, ulSize);

            DosClose(hFile);
        }
    } // end if ((pszFile) && (pszContent))

    if (pulWritten)
        *pulWritten = ulWritten;

    return arc;
}

/*
 *@@category: Helpers\Control program helpers\Directory management
 *      directory helpers (querying, creating, deleting etc.).
 */

/* ******************************************************************
 *
 *   Directory helpers
 *
 ********************************************************************/

/*
 *@@ doshQueryDirExist:
 *      returns TRUE if the given directory
 *      exists and really is a directory.
 */

BOOL doshQueryDirExist(PCSZ pcszDir)
{
    FILESTATUS3 fs3;
    APIRET arc;

    if (!(arc = DosQueryPathInfo((PSZ)pcszDir,
                                 FIL_STANDARD,
                                 &fs3,
                                 sizeof(fs3))))
        // file found:
        return ((fs3.attrFile & FILE_DIRECTORY) != 0);

    return FALSE;
}

/*
 *@@ doshCreatePath:
 *      this creates the specified directory.
 *      As opposed to DosCreateDir, this
 *      function can create several directories
 *      at the same time, if the parent
 *      directories do not exist yet.
 */

APIRET doshCreatePath(PCSZ pcszPath,
                      BOOL fHidden) // in: if TRUE, the new directories will get FILE_HIDDEN
{
    APIRET  arc0 = NO_ERROR;
    CHAR    path[CCHMAXPATH];
    CHAR    *cp, c;
    ULONG   cbPath;

    strcpy(path, pcszPath);
    cbPath = strlen(path);

    if (path[cbPath] != '\\')
    {
        path[cbPath] = '\\';
        path[cbPath + 1] = 0;
    }

    cp = path;
    // advance past the drive letter only if we have one
    if (*(cp+1) == ':')
        cp += 3;

    // go!
    while (*cp != 0)
    {
        if (*cp == '\\')
        {
            c = *cp;
            *cp = 0;
            if (!doshQueryDirExist(path))
            {
                APIRET arc = DosCreateDir(path,
                                          0);   // no EAs
                if (arc != NO_ERROR)
                {
                    arc0 = arc;
                    break;
                }
                else
                    if (fHidden)
                    {
                        // hide the directory we just created
                        doshSetPathAttr(path, FILE_HIDDEN);
                    }
            }
            *cp = c;
        }
        cp++;
    }

    return arc0;
}

/*
 *@@ doshQueryCurrentDir:
 *      writes the current directory into
 *      the specified buffer, which should be
 *      CCHMAXPATH in size.
 *
 *      As opposed to DosQueryCurrentDir, this
 *      includes the drive letter.
 *
 *@@added V0.9.4 (2000-07-22) [umoeller]
 */

APIRET doshQueryCurrentDir(PSZ pszBuf)
{
    APIRET arc = NO_ERROR;
    ULONG   ulCurDisk = 0;
    ULONG   ulMap = 0;
    if (!(arc = DosQueryCurrentDisk(&ulCurDisk, &ulMap)))
    {
        ULONG   cbBuf = CCHMAXPATH - 3;
        pszBuf[0] = ulCurDisk + 'A' - 1;
        pszBuf[1] = ':';
        pszBuf[2] = '\\';
        pszBuf[3] = '\0';
        arc = DosQueryCurrentDir(0, pszBuf + 3, &cbBuf);
    }

    return arc;
}

/*
 *@@ doshDeleteDir:
 *      deletes a directory. As opposed to DosDeleteDir,
 *      this removes empty subdirectories and/or files
 *      as well.
 *
 *      flFlags can be any combination of the following:
 *
 *      -- DOSHDELDIR_RECURSE: recurse into subdirectories.
 *
 *      -- DOSHDELDIR_DELETEFILES: delete all regular files
 *              which are found on the way.
 *
 *              THIS IS NOT IMPLEMENTED YET.
 *
 *      If 0 is specified, this effectively behaves just as
 *      DosDeleteDir.
 *
 *      If you specify DOSHDELDIR_RECURSE only, only empty
 *      subdirectories are deleted as well.
 *
 *      If you specify DOSHDELDIR_RECURSE | DOSHDELDIR_DELETEFILES,
 *      this removes an entire directory tree, including all
 *      subdirectories and files.
 *
 *@@added V0.9.4 (2000-07-01) [umoeller]
 *@@changed WarpIN V1.0.15 (2007-03-27) [pr]: allow NULL pulDirs and pulFiles
 *@@changed WarpIN V1.0.16 (2007-07-28) [pr]: bump directory count on recursive delete failures @@fixes 1001
 *@@changed WarpIN V1.0.20 (2012-03-29) [pr]: path not found no longer an error
 */

APIRET doshDeleteDir(PCSZ pcszDir,
                     ULONG flFlags,
                     PULONG pulDirs,        // out: directories found
                     PULONG pulFiles)       // out: files found
{
    APIRET          arc = NO_ERROR,
                    arcReturn = NO_ERROR;

    HDIR            hdirFindHandle = HDIR_CREATE;
    FILEFINDBUF3    ffb3     = {0};      // returned from FindFirst/Next
    ULONG           ulResultBufLen = sizeof(FILEFINDBUF3);
    ULONG           ulFindCount    = 1;        // look for 1 file at a time

    CHAR            szFileMask[2*CCHMAXPATH];
    sprintf(szFileMask, "%s\\*", pcszDir);

    // find files
    arc = DosFindFirst(szFileMask,
                       &hdirFindHandle,     // directory search handle
                       FILE_ARCHIVED | FILE_DIRECTORY | FILE_SYSTEM
                         | FILE_HIDDEN | FILE_READONLY,
                                     // search attributes; include all, even dirs
                       &ffb3,               // result buffer
                       ulResultBufLen,      // result buffer length
                       &ulFindCount,        // number of entries to find
                       FIL_STANDARD);       // return level 1 file info

    if (arc == NO_ERROR)
    {
        // keep finding the next file until there are no more files
        while (arc == NO_ERROR)     // != ERROR_NO_MORE_FILES
        {
            if (ffb3.attrFile & FILE_DIRECTORY)
            {
                // we found a directory:

                // ignore the pseudo-directories
                if (    (strcmp(ffb3.achName, ".") != 0)
                     && (strcmp(ffb3.achName, "..") != 0)
                   )
                {
                    // real directory:
                    if (flFlags & DOSHDELDIR_RECURSE)
                    {
                        // recurse!
                        CHAR szSubDir[2*CCHMAXPATH];
                        sprintf(szSubDir, "%s\\%s", pcszDir, ffb3.achName);
                        arcReturn = doshDeleteDir(szSubDir,
                                                  flFlags,
                                                  pulDirs,
                                                  pulFiles);
                        // this removes ffb3.achName as well
                    }
                    else
                    {
                        // directory, but no recursion:
                        // report "access denied"
                        // (this is what OS/2 reports with DosDeleteDir as well)
                        arcReturn = ERROR_ACCESS_DENIED;  // 5
                    }

                    if (arcReturn != NO_ERROR)  // WarpIN V1.0.16 (2007-07-28) [pr]
                        if (pulDirs)  // WarpIN V1.0.15 (2007-03-27) [pr]
                            (*pulDirs)++;
                }
            }
            else
            {
                // it's a file:
                arcReturn = ERROR_ACCESS_DENIED;
                if (pulFiles)  // WarpIN V1.0.15 (2007-03-27) [pr]
                    (*pulFiles)++;
            }

            if (arc == NO_ERROR)
            {
                ulFindCount = 1;                      // reset find count
                arc = DosFindNext(hdirFindHandle,      // directory handle
                                  &ffb3,         // result buffer
                                  ulResultBufLen,      // result buffer length
                                  &ulFindCount);       // number of entries to find
            }
        } // endwhile

        DosFindClose(hdirFindHandle);    // close our find handle
    }

    if (arcReturn == NO_ERROR)
    {
        // success so far:
        // delete our directory now
        arcReturn = DosDeleteDir((PSZ)pcszDir);
        if (arcReturn == ERROR_PATH_NOT_FOUND)  // WarpIN V1.0.20 (2012-03-29) [pr]
            arcReturn = NO_ERROR;
    }

    return arcReturn;
}

/*
 *@@ doshCanonicalize:
 *      simplifies path specifications to remove '.'
 *      and '..' entries and generates a fully
 *      qualified path name where possible.
 *      File specifications are left unchanged.
 *
 *      This returns:
 *
 *      --  NO_ERROR: the buffers were valid.
 *
 *      --  ERROR_INVALID_PARAMETER: the buffers
 *          were invalid.
 *
 *@@added V0.9.19 (2002-04-22) [pr]
 */

APIRET doshCanonicalize(PCSZ pcszFileIn,        // in: path to canonicalize
                        PSZ pszFileOut,         // out: canonicalized path if NO_ERROR
                        ULONG cbFileOut)        // in: size of pszFileOut buffer
{
    APIRET ulrc = NO_ERROR;
    CHAR szFileTemp[CCHMAXPATH];

    if (pcszFileIn && pszFileOut && cbFileOut)
    {
        strncpy(szFileTemp, pcszFileIn, sizeof(szFileTemp) - 1);
        szFileTemp[sizeof(szFileTemp) - 1] = 0;
        if (    strchr(szFileTemp, '\\')
             || strchr(szFileTemp, ':')
           )
        {
            ULONG cbFileTemp = strlen(szFileTemp);

            if (    (cbFileTemp > 3)
                 && (szFileTemp[cbFileTemp - 1] == '\\')
               )
            {
                szFileTemp[cbFileTemp - 1] = 0;
            }

            if (DosQueryPathInfo(szFileTemp,
                                 FIL_QUERYFULLNAME,
                                 pszFileOut,
                                 cbFileOut))
            {
                pszFileOut[0] = 0;
            }
        }
        else
        {
            strncpy(pszFileOut, pcszFileIn, cbFileOut - 1);
            pszFileOut[cbFileOut - 1] = 0;
        }
    }
    else
        ulrc = ERROR_INVALID_PARAMETER;

    return(ulrc);
}

#define FINDBUFSIZE             0x10000     // 64K
#define FINDCOUNT               500

/*
 *@@ doshForAllFiles:
 *      this calles pfncb for all files in a directory matching
 *      the given file mask.
 *
 *      This is to avoid having to recode the typical but
 *      easy-to-get-wrong DosFindFirst/Next loop.
 *
 *      pfncb must be prototyped as follows:
 *
 +          APIRET XWPENTRY fnMyCallback(const FILEFINDBUF3 *pfb3,
 +                                       PVOID pvCallback)
 *
 *      On each iteration, it receives the current file-find
 *      buffer in pfb3. pvCallback is constantly set to what
 *      was passed in to this function.
 *
 *      The callback will get called for every file returned
 *      from the loop. This function will automatically
 *      filter out the stupid "." and ".." directory entries
 *      that DosFindFirst/Next always return, so the callback
 *      will never see those.
 *
 *      If the callback returns any value other than NO_ERROR,
 *      this function aborts and returns that error. The
 *      exception is that if the callback returns
 *      ERROR_NO_MORE_FILES, this function will abort also,
 *      but return NO_ERROR still. This is useful if you are
 *      looking for a specific file and want to cancel the
 *      search early without provoking an error.
 *
 *@@added V1.0.2 (2003-02-03) [umoeller]
 */

APIRET doshForAllFiles(PCSZ pcszSearchMask,         // in: search mask (e.g. "C:\dir\*.txt")
                       ULONG flFile,                // in: any of FILE_ARCHIVED | FILE_HIDDEN | FILE_SYSTEM | FILE_READONLY | FILE_DIRECTORY
                       FNCBFORALLFILES *pfncb,      // in: address of callback function
                       PVOID pvCallback)            // in: parameter passed to callback
{
    APIRET  arc = NO_ERROR;
    HDIR    hdirFindHandle = HDIR_CREATE;
    ULONG   ulFindCount = FINDCOUNT;

    PBYTE   pbFindBuf;

    if (arc = DosAllocMem((PVOID*)&pbFindBuf,
                          FINDBUFSIZE,
                          PAG_COMMIT | PAG_READ | PAG_WRITE | OBJ_TILE))
        return arc;

    arc = DosFindFirst((PSZ)pcszSearchMask,
                       &hdirFindHandle,
                       flFile,
                       pbFindBuf,
                       FINDBUFSIZE,
                       &ulFindCount,
                       FIL_STANDARD);

    while (    (arc == NO_ERROR)
            || (arc == ERROR_BUFFER_OVERFLOW)
          )
    {
        ULONG           ul;
        PFILEFINDBUF3   pfb3 = (PFILEFINDBUF3)pbFindBuf;

        for (ul = 0;
             ul < ulFindCount;
             ul++)
        {
            // filter out the "." and ".." entries
            if (!(    (pfb3->attrFile & FILE_DIRECTORY)
                   && (pfb3->achName[0] == '.')
                   && (    (pfb3->achName[1] == '\0')
                        || (    (pfb3->achName[1] == '.')
                             && (pfb3->achName[2] == '\0')
                           )
                      )
               ))
            {
                // call callback
                if (arc = pfncb(pfb3, pvCallback))
                    // callback returned error:
                    break;
            }

            // next item in buffer
            if (pfb3->oNextEntryOffset)
                pfb3 = (PFILEFINDBUF3)(   (PBYTE)pfb3
                                        + pfb3->oNextEntryOffset
                                      );
        }

        if (!arc)
        {
            ulFindCount = FINDCOUNT;
            arc = DosFindNext(hdirFindHandle,
                              pbFindBuf,
                              FINDBUFSIZE,
                              &ulFindCount);
        }
    }

    // no more files is not an error
    if (arc == ERROR_NO_MORE_FILES)
        arc = NO_ERROR;

    DosFindClose(hdirFindHandle);

    DosFreeMem(pbFindBuf);

    return arc;
}

/*
 *@@category: Helpers\Control program helpers\Module handling
 *      helpers for importing functions from a module (DLL).
 */

/* ******************************************************************
 *
 *   Module handling helpers
 *
 ********************************************************************/

/*
 *@@ doshQueryProcAddr:
 *      attempts to resolve the given procedure from
 *      the given module name. Saves you from querying
 *      the module handle and all that.
 *
 *      This is intended for resolving undocumented
 *      APIs from OS/2 system DLls such as PMMERGE
 *      and DOSCALLS. It is assumed that the specified
 *      module is already loaded.
 *
 *      Returns:
 *
 *      --  NO_ERROR
 *
 *      --  ERROR_INVALID_NAME
 *
 *      --  ERROR_INVALID_ORDINAL
 *
 *      plus the error codes of DosLoadModule.
 *
 *@@added V0.9.16 (2002-01-13) [umoeller]
 */

APIRET doshQueryProcAddr(PCSZ pcszModuleName,       // in: module name (e.g. "PMMERGE")
                         ULONG ulOrdinal,           // in: proc ordinal
                         PFN *ppfn)                 // out: proc address
{
    HMODULE hmod;
    APIRET  arc;
    if (!(arc = DosQueryModuleHandle((PSZ)pcszModuleName,
                                     &hmod)))
    {
        if ((arc = DosQueryProcAddr(hmod,
                                    ulOrdinal,
                                    NULL,
                                    ppfn)))
        {
            // the CP programming guide and reference says use
            // DosLoadModule if DosQueryProcAddr fails with this error
            if (arc == ERROR_INVALID_HANDLE)
            {
                if (!(arc = DosLoadModule(NULL,
                                          0,
                                          (PSZ)pcszModuleName,
                                          &hmod)))
                {
                    arc = DosQueryProcAddr(hmod,
                                           ulOrdinal,
                                           NULL,
                                           ppfn);
                }
            }
        }
    }

    return arc;
}

/*
 *@@ doshResolveImports:
 *      this function loads the module called pszModuleName
 *      and resolves imports dynamically using DosQueryProcAddress.
 *
 *      To specify the functions to be imported, a RESOLVEFUNCTION
 *      array is used. In each of the array items, specify the
 *      name of the function and a pointer to a function pointer
 *      where to store the resolved address.
 *
 *@@added V0.9.3 (2000-04-29) [umoeller]
 */

APIRET doshResolveImports(PCSZ pcszModuleName,    // in: DLL to load
                          HMODULE *phmod,       // out: module handle
                          PCRESOLVEFUNCTION paResolves, // in/out: function resolves
                          ULONG cResolves)      // in: array item count (not array size!)
{
    CHAR    szName[CCHMAXPATH];
    APIRET  arc;

    if (!(arc = DosLoadModule(szName,
                              sizeof(szName),
                              (PSZ)pcszModuleName,
                              phmod)))
    {
        ULONG  ul;
        for (ul = 0;
             ul < cResolves;
             ul++)
        {
            if (arc = DosQueryProcAddr(*phmod,
                                       0,               // ordinal, ignored
                                       (PSZ)paResolves[ul].pcszFunctionName,
                                       paResolves[ul].ppFuncAddress))
                // error:
                break;
        }

        if (arc)
            // V0.9.16 (2001-12-08) [umoeller]
            DosFreeModule(*phmod);
    }

    return arc;
}

/*
 *@@category: Helpers\Control program helpers\Performance (CPU load) helpers
 *      helpers around DosPerfSysCall.
 */

/* ******************************************************************
 *
 *   Performance Counters (CPU Load)
 *
 ********************************************************************/

/*
 *@@ doshPerfOpen:
 *      initializes the OS/2 DosPerfSysCall API for
 *      the calling thread.
 *
 *      Note: This API is not supported on all OS/2
 *      versions. I believe it came up with some Warp 4
 *      fixpak. The API is resolved dynamically by
 *      this function (using DosQueryProcAddr).
 *
 *      This properly initializes the internal counters
 *      which the OS/2 kernel uses for this API. Apparently,
 *      with newer kernels (FP13/14), IBM has chosen to no
 *      longer do this automatically, which is the reason
 *      why many "pulse" utilities display garbage with these
 *      fixpaks.
 *
 *      After NO_ERROR is returned, DOSHPERFSYS.cProcessors
 *      contains the no. of processors found on the system.
 *      All pointers in DOSHPERFSYS then point to arrays
 *      which have exactly cProcessors array items.
 *
 *      So after NO_ERROR was returned here, you can keep
 *      calling doshPerfGet to get a current snapshot of the
 *      IRQ and user loads for each CPU. Note that interrupts
 *      are only processed on CPU 0 on SMP systems.
 *
 *      Call doshPerfClose to clean up resources allocated
 *      by this function.
 *
 *      For more sample code, take a look at the "Pulse" widget
 *      in the XWorkplace sources (src\xcenter\w_pulse.c).
 *
 *      Example code:
 *
 +      PDOSHPERFSYS pPerf = NULL;
 +      APIRET arc;
 +      if (!(arc = doshPerfOpen(&pPerf)))
 +      {
 +          // this should really be in a timer,
 +          // e.g. once per second
 +          ULONG   ulCPU;
 +          if (!(arc = doshPerfGet(&pPerf)))
 +          {
 +              // go thru all CPUs
 +              for (ulCPU = 0; ulCPU < pPerf->cProcessors; ulCPU++)
 +              {
 +                  LONG lUserLoadThis = pPerf->palLoads[ulCPU];
 +                  ...
 +              }
 +          }
 +
 +          // clean up
 +          doshPerfClose(&pPerf);
 +      }
 +
 *
 *@@added V0.9.7 (2000-12-02) [umoeller]
 *@@changed V0.9.9 (2001-03-14) [umoeller]: added interrupt loads; thanks phaller
 */

APIRET doshPerfOpen(PDOSHPERFSYS *ppPerfSys)  // out: new DOSHPERFSYS structure
{
    APIRET  arc = NO_ERROR;

    // allocate DOSHPERFSYS structure
    if (!(*ppPerfSys = (PDOSHPERFSYS)malloc(sizeof(DOSHPERFSYS))))
        arc = ERROR_NOT_ENOUGH_MEMORY;
    else
    {
        // initialize structure
        PDOSHPERFSYS pPerfSys = *ppPerfSys;
        memset(pPerfSys, 0, sizeof(*pPerfSys));

        // resolve DosPerfSysCall API entry
        if (!(arc = DosLoadModule(NULL,
                                  0,
                                  "DOSCALLS",
                                  &pPerfSys->hmod)))
        {
            if (!(arc = DosQueryProcAddr(pPerfSys->hmod,
                                         976,
                                         "DosPerfSysCall",
                                         (PFN*)&pPerfSys->pDosPerfSysCall)))
            {
                // OK, we got the API: initialize!
                if (!(arc = pPerfSys->pDosPerfSysCall(CMD_KI_ENABLE,
                                                      0,
                                                      0,
                                                      0)))
                {
                    pPerfSys->fInitialized = TRUE;
                            // call CMD_KI_DISABLE later

                    if (!(arc = pPerfSys->pDosPerfSysCall(CMD_PERF_INFO,
                                                          0,
                                                          (ULONG)&pPerfSys->cProcessors,
                                                          0)))
                    {
                        ULONG   ul = 0,
                                cProcs = pPerfSys->cProcessors,
                                cbDouble = cProcs * sizeof(double),
                                cbLong = cProcs * sizeof(LONG);

                        // allocate arrays
                        if (    (!(pPerfSys->paCPUUtils = (PCPUUTIL)calloc(cProcs,
                                                                sizeof(CPUUTIL))))
                             || (!(pPerfSys->padBusyPrev
                                    = (double*)malloc(cbDouble)))
                             || (!(pPerfSys->padTimePrev
                                    = (double*)malloc(cbDouble)))
                             || (!(pPerfSys->padIntrPrev
                                    = (double*)malloc(cbDouble)))
                             || (!(pPerfSys->palLoads
                                    = (PLONG)malloc(cbLong)))
                             || (!(pPerfSys->palIntrs
                                    = (PLONG)malloc(cbLong)))
                           )
                            arc = ERROR_NOT_ENOUGH_MEMORY;
                        else
                        {
                            for (ul = 0; ul < cProcs; ul++)
                            {
                                pPerfSys->padBusyPrev[ul] = 0.0;
                                pPerfSys->padTimePrev[ul] = 0.0;
                                pPerfSys->padIntrPrev[ul] = 0.0;
                                pPerfSys->palLoads[ul] = 0;
                                pPerfSys->palIntrs[ul] = 0;
                            }
                        }
                    }
                }
            } // end if (arc == NO_ERROR)
        } // end if (arc == NO_ERROR)

        if (arc)
            // error: clean up
            doshPerfClose(ppPerfSys);

    } // end else if (!*ppPerfSys)

    return arc;
}

/*
 *@@ doshPerfGet:
 *      calculates a current snapshot of the system load,
 *      compared with the load which was calculated on
 *      the previous call.
 *
 *      If you want to continually measure the system CPU
 *      load, this is the function you will want to call
 *      regularly -- e.g. with a timer once per second.
 *
 *      Call this ONLY if doshPerfOpen returned NO_ERROR,
 *      or you'll get crashes.
 *
 *      If this call returns NO_ERROR, you get LONG load
 *      values for each CPU in the system in the arrays
 *      in DOSHPERFSYS (in per-mille, 0-1000).
 *
 *      There are two arrays:
 *
 *      -- DOSHPERFSYS.palLoads contains the "user" load
 *         for each CPU.
 *
 *      -- DOSHPERFSYS.palIntrs contains the "IRQ" load
 *         for each CPU.
 *
 *      Sum up the two values to get the total load for
 *      each CPU.
 *
 *      For example, if there are two CPUs, after this call,
 *
 *      -- DOSHPERFSYS.palLoads[0] contains the "user" load
 *         of the first CPU,
 *
 *      -- DOSHPERFSYS.palLoads[0] contains the "user" load
 *         of the second CPU.
 *
 *      See doshPerfOpen for example code.
 *
 *@@added V0.9.7 (2000-12-02) [umoeller]
 *@@changed V0.9.9 (2001-03-14) [umoeller]: added interrupt loads; thanks phaller
 */

APIRET doshPerfGet(PDOSHPERFSYS pPerfSys)
{
    APIRET arc = NO_ERROR;
    if (!pPerfSys->pDosPerfSysCall)
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        if (!(arc = pPerfSys->pDosPerfSysCall(CMD_KI_RDCNT,
                                              (ULONG)pPerfSys->paCPUUtils,
                                              0, 0)))
        {
            // go thru all processors
            ULONG ul = 0;
            for (; ul < pPerfSys->cProcessors; ul++)
            {
                PCPUUTIL    pCPUUtilThis = &pPerfSys->paCPUUtils[ul];

                double      dTime = LL2F(pCPUUtilThis->ulTimeHigh,
                                         pCPUUtilThis->ulTimeLow);
                double      dBusy = LL2F(pCPUUtilThis->ulBusyHigh,
                                         pCPUUtilThis->ulBusyLow);
                double      dIntr = LL2F(pCPUUtilThis->ulIntrHigh,
                                         pCPUUtilThis->ulIntrLow);

                double      *pdBusyPrevThis = &pPerfSys->padBusyPrev[ul];
                double      *pdTimePrevThis = &pPerfSys->padTimePrev[ul];
                double      *pdIntrPrevThis = &pPerfSys->padIntrPrev[ul];

                // avoid division by zero
                double      dTimeDelta;
                if (dTimeDelta = (dTime - *pdTimePrevThis))
                {
                    pPerfSys->palLoads[ul]
                        = (LONG)( (double)(   (dBusy - *pdBusyPrevThis)
                                            / dTimeDelta
                                            * 1000.0
                                          )
                                );
                    pPerfSys->palIntrs[ul]
                        = (LONG)( (double)(   (dIntr - *pdIntrPrevThis)
                                            / dTimeDelta
                                            * 1000.0
                                          )
                                );
                }
                else
                {
                    // no clear readings are available
                    pPerfSys->palLoads[ul] = 0;
                    pPerfSys->palIntrs[ul] = 0;
                }

                *pdTimePrevThis = dTime;
                *pdBusyPrevThis = dBusy;
                *pdIntrPrevThis = dIntr;
            }
        }
    }

    return arc;
}

/*
 *@@ doshPerfClose:
 *      frees all resources allocated by doshPerfOpen.
 *
 *@@added V0.9.7 (2000-12-02) [umoeller]
 *@@changed V0.9.9 (2001-02-06) [umoeller]: removed disable; this broke the WarpCenter
 *@@changed V0.9.9 (2001-03-14) [umoeller]: fixed memory leak
 *@@changed V0.9.9 (2001-03-14) [umoeller]: added interrupt loads; thanks phaller
 */

APIRET doshPerfClose(PDOSHPERFSYS *ppPerfSys)
{
    APIRET arc = NO_ERROR;
    PDOSHPERFSYS pPerfSys;
    if (    (!ppPerfSys)
         || (!(pPerfSys = *ppPerfSys))
       )
        arc = ERROR_INVALID_PARAMETER;
    else
    {
        // do not call this, this messes up the WarpCenter V0.9.9 (2001-02-06) [umoeller]
        // if (pPerfSys->fInitialized) pPerfSys->pDosPerfSysCall(CMD_KI_DISABLE, 0, 0, 0);

        if (pPerfSys->paCPUUtils)
            free(pPerfSys->paCPUUtils);
        if (pPerfSys->padBusyPrev)
            free(pPerfSys->padBusyPrev);
        if (pPerfSys->padTimePrev)
            free(pPerfSys->padTimePrev);
        if (pPerfSys->padIntrPrev)
            free(pPerfSys->padIntrPrev);
        if (pPerfSys->palLoads)             // was missing V0.9.9 (2001-03-14) [umoeller]
            free(pPerfSys->palLoads);
        if (pPerfSys->palIntrs)
            free(pPerfSys->palIntrs);

        if (pPerfSys->hmod)
            DosFreeModule(pPerfSys->hmod);
        free(pPerfSys);
        *ppPerfSys = NULL;
    }

    return arc;
}

/*
 *@@category: Helpers\Control program helpers\Process management
 *      helpers for starting subprocesses.
 */

/* ******************************************************************
 *
 *   Process helpers
 *
 ********************************************************************/

STATIC PVOID    G_pvLocalInfoSeg = NULL;

USHORT _Far16 _Pascal Dos16GetInfoSeg(PSEL pselGlobal,
                                      PSEL pselLocal);

/*
 * GetInfoSegs:
 *
 */

STATIC VOID GetInfoSegs(VOID)
{
    SEL     GlobalInfoSegSelector,
            LocalInfoSegSelector;

    // get selectors (old 16-bit API; this gets called only once)
    Dos16GetInfoSeg(&GlobalInfoSegSelector,
                    &LocalInfoSegSelector);
    // thunk
    /* G_pvGlobalInfoSeg = (PVOID)(   (GlobalInfoSegSelector << 0x000D)
                                 & 0x01fff0000); */
    G_pvLocalInfoSeg  = (PVOID)(   (LocalInfoSegSelector << 0x000D)
                                 & 0x01fff0000);
}

/*
 *@@ doshMyPID:
 *      returns the PID of the current process.
 *
 *      This uses an interesting hack which is way
 *      faster than DosGetInfoBlocks.
 *
 *@@added V0.9.9 (2001-04-04) [umoeller]
 */

ULONG doshMyPID(VOID)
{
    if (!G_pvLocalInfoSeg)
        // first call:
        GetInfoSegs();

    // PID is at offset 0 in the local info seg
    return *(PUSHORT)G_pvLocalInfoSeg;
}

/*
 *@@ doshMyParentPID:
 *      returns the PID of the parent of the current process.
 *
 *      This uses an interesting hack which is way
 *      faster than DosGetInfoBlocks.
 *
 *@@added V1.0.2 (2003-11-13) [umoeller]
 */

ULONG doshMyParentPID(VOID)
{
    if (!G_pvLocalInfoSeg)
        // first call:
        GetInfoSegs();

    // parent PID is at offset 2 in the local info seg
    return *(PUSHORT)((PBYTE)G_pvLocalInfoSeg + 2);
}

/*
 *@@ doshMyTID:
 *      returns the TID of the current thread.
 *
 *      This uses an interesting hack which is way
 *      faster than DosGetInfoBlocks.
 *
 *@@added V0.9.9 (2001-04-04) [umoeller]
 */

ULONG doshMyTID(VOID)
{
    if (!G_pvLocalInfoSeg)
        // first call:
        GetInfoSegs();

    // TID is at offset 6 in the local info seg
    return *(PUSHORT)((PBYTE)G_pvLocalInfoSeg + 6);
}

/*
 *@@ doshExecVIO:
 *      executes cmd.exe with the /c parameter
 *      and pcszExecWithArgs. This is equivalent
 *      to the C library system() function,
 *      except that an OS/2 error code is returned
 *      and that this works with the VAC subsystem
 *      library.
 *
 *      This uses DosExecPgm and handles the sick
 *      argument passing.
 *
 *      If NO_ERROR is returned, *plExitCode receives
 *      the exit code of the process. If the process
 *      was terminated abnormally, *plExitCode receives:
 *
 *      -- -1: hard error halt
 *      -- -2: 16-bit trap
 *      -- -3: DosKillProcess
 *      -- -4: 32-bit exception
 *
 *@@added V0.9.4 (2000-07-27) [umoeller]
 */

APIRET doshExecVIO(PCSZ pcszExecWithArgs,
                   PLONG plExitCode)            // out: exit code (ptr can be NULL)
{
    APIRET arc = NO_ERROR;

    if (strlen(pcszExecWithArgs) > 255)
        arc = ERROR_INSUFFICIENT_BUFFER;
    else
    {
        CHAR szObj[CCHMAXPATH];
        RESULTCODES res = {0};
        CHAR szBuffer[300];

        // DosExecPgm expects two args in szBuffer:
        // --  cmd.exe\0
        // --  then the actual argument, terminated by two 0 bytes.
        memset(szBuffer, 0, sizeof(szBuffer));
        strcpy(szBuffer, "cmd.exe\0");
        sprintf(szBuffer + 8, "/c %s",
                pcszExecWithArgs);

        arc = DosExecPgm(szObj, sizeof(szObj),
                         EXEC_SYNC,
                         szBuffer,
                         0,
                         &res,
                         "cmd.exe");
        if ((arc == NO_ERROR) && (plExitCode))
        {
            if (res.codeTerminate == 0)
                // normal exit:
                *plExitCode = res.codeResult;
            else
                *plExitCode = -((LONG)res.codeTerminate);
        }
    }

    return arc;
}

/*
 *@@ doshQuickStartSession:
 *      this is a shortcut to DosStartSession w/out having to
 *      deal with all the messy parameters.
 *
 *      This one starts pszPath as a child session and passes
 *      pszParams to it.
 *
 *      In usPgmCtl, OR any or none of the following (V0.9.0):
 *      --  SSF_CONTROL_NOAUTOCLOSE (0x0008): do not close automatically.
 *              This bit is used only for VIO Windowable apps and ignored
 *              for all other applications.
 *      --  SSF_CONTROL_MINIMIZE (0x0004)
 *      --  SSF_CONTROL_MAXIMIZE (0x0002)
 *      --  SSF_CONTROL_INVISIBLE (0x0001)
 *      --  SSF_CONTROL_VISIBLE (0x0000)
 *
 *      Specifying 0 will therefore auto-close the session and make it
 *      visible.
 *
 *      If (fWait), this function will create a termination queue
 *      and not return until the child session has ended. Be warned,
 *      this blocks the calling thread.
 *
 *      Otherwise the function will return immediately.
 *
 *      The session and process IDs of the child session will be
 *      written to *pulSID and *ppid. Of course, in "wait" mode,
 *      these are no longer valid after this function returns.
 *
 *      Returns the error code of DosStartSession.
 *
 *      Note: According to CPREF, calling DosStartSession calls
 *      DosExecPgm in turn for all full-screen, VIO, and PM sessions.
 *
 *@@changed V0.9.0 [umoeller]: prototype changed to include usPgmCtl
 *@@changed V0.9.1 (99-12-30) [umoeller]: queue was sometimes not closed. Fixed.
 *@@changed V0.9.3 (2000-05-03) [umoeller]: added fForeground
 *@@changed V0.9.14 (2001-08-03) [umoeller]: fixed potential queue leak
 *@@changed V0.9.14 (2001-08-03) [umoeller]: fixed memory leak in wait mode; added pusReturn to prototype
 *@@changed V1.0.2  (2003-02-04) [jsmall  ]: inserted new 3rd parameter for session type
 *@@changed V1.0.2  (2003-02-04) [jsmall  ]: changed code so when fWait = FALSE then session is started as independent, not as a child
 *@@changed V1.0.5 (2006-04-01) [pr]: Added pcszTitle parameter
 */

APIRET doshQuickStartSession(PCSZ pcszPath,         // in: program to start
                             PCSZ pcszParams,       // in: parameters for program
                             PCSZ pcszTitle,        // in: window title
                             USHORT usSessionType,  // in: session type
                             BOOL fForeground,      // in: if TRUE, session will be in foreground
                             USHORT usPgmCtl,       // in: STARTDATA.PgmControl
                             BOOL fWait,            // in: wait for termination?
                             PULONG pulSID,         // out: session ID (req.)
                             PPID ppid,             // out: process ID (req.)
                             PUSHORT pusReturn)     // out: in wait mode, session's return code (ptr can be NULL)
{
    APIRET      arc = NO_ERROR;
    // queue stuff
    const char  *pcszQueueName = "\\queues\\xwphlpsw.que";
    HQUEUE      hq = 0;
    PID         qpid = 0;

    if (fWait)
    {
        if (!(arc = DosCreateQueue(&hq,
                                   QUE_FIFO | QUE_CONVERT_ADDRESS,
                                   (PSZ)pcszQueueName)))
            arc = DosOpenQueue(&qpid, &hq, (PSZ)pcszQueueName);
    }

    if (!arc)    // V0.9.14 (2001-08-03) [umoeller]
    {
        STARTDATA   SData;
        CHAR        szObjBuf[CCHMAXPATH];

        SData.Length  = sizeof(STARTDATA);
//      SData.Related = SSF_RELATED_CHILD; //INDEPENDENT;
        SData.Related = (fWait) ? SSF_RELATED_CHILD : SSF_RELATED_INDEPENDENT;
                // V1.0.2 (2003-02-04) [jsmall]
        SData.FgBg    = (fForeground) ? SSF_FGBG_FORE : SSF_FGBG_BACK;
                // V0.9.3 (2000-05-03) [umoeller]
        SData.TraceOpt = SSF_TRACEOPT_NONE;

        // V1.0.5 (2006-04-01) [pr]
        if (pcszTitle && *pcszTitle)
            SData.PgmTitle = (PSZ)pcszTitle;      // title for window
        else
            SData.PgmTitle = (PSZ)pcszPath;

        SData.PgmName = (PSZ)pcszPath;
        SData.PgmInputs = (PSZ)pcszParams;

        SData.TermQ = (fWait) ? (PSZ)pcszQueueName : NULL;
        SData.Environment = 0;
        SData.InheritOpt = SSF_INHERTOPT_PARENT;
//      SData.SessionType = SSF_TYPE_DEFAULT;
        SData.SessionType = usSessionType;    // V1.0.2 (2003-02-04)  [jsmall]
        SData.IconFile = 0;
        SData.PgmHandle = 0;

        SData.PgmControl = usPgmCtl;

        SData.InitXPos  = 30;
        SData.InitYPos  = 40;
        SData.InitXSize = 200;
        SData.InitYSize = 140;
        SData.Reserved = 0;
        SData.ObjectBuffer  = szObjBuf;
        SData.ObjectBuffLen = (ULONG)sizeof(szObjBuf);

        if (    (!(arc = DosStartSession(&SData, pulSID, ppid)))
             && (fWait)
           )
        {
            // block on the termination queue, which is written
            // to when the subprocess ends
            REQUESTDATA rqdata;
            ULONG       cbData = 0;
            PULONG      pulData = NULL;
            BYTE        elpri;

            rqdata.pid = qpid;
            if (!(arc = DosReadQueue(hq,                // in: queue handle
                                     &rqdata,           // out: pid and ulData
                                     &cbData,           // out: size of data returned
                                     (PVOID*)&pulData,  // out: data returned
                                     0,                 // in: remove first element in queue
                                     0,                 // in: wait for queue data (block thread)
                                     &elpri,            // out: element's priority
                                     0)))               // in: event semaphore to be posted
            {
                if (!rqdata.ulData)
                {
                    // child session ended:
                    // V0.9.14 (2001-08-03) [umoeller]

                    // *pulSID = (*pulData) & 0xffff;
                    if (pusReturn)
                        *pusReturn = ((*pulData) >> 16) & 0xffff;

                }
                // else: continue looping

                if (pulData)
                    DosFreeMem(pulData);
            }
        }
    }

    if (hq)
        DosCloseQueue(hq);

    return arc;
}


