/****************************************************************/
/*                                                              */
/*                          inthndlr.c                          */
/*                                                              */
/*    Interrupt Handler and Function dispatcher for Kernel      */
/*                                                              */
/*                      Copyright (c) 1995                      */
/*                      Pasquale J. Villani                     */
/*                      All Rights Reserved                     */
/*                                                              */
/* This file is part of DOS-C.                                  */
/*                                                              */
/* DOS-C 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; either version */
/* 2, or (at your option) any later version.                    */
/*                                                              */
/* DOS-C 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.             */
/*                                                              */
/* You should have received a copy of the GNU General Public    */
/* License along with DOS-C; see the file COPYING.  If not,     */
/* write to the Free Software Foundation, 675 Mass Ave,         */
/* Cambridge, MA 02139, USA.                                    */
/*                                                              */
/****************************************************************/
#define MAIN

#include "portab.h"
#include "globals.h"
#include "nls.h"
#include "win.h"
#include "debug.h"

#ifdef VERSION_STRINGS
BYTE *RcsId =
    "$Id: inthndlr.c 1709 2012-02-08 02:13:49Z perditionc $";
#endif

#ifdef TSC
STATIC VOID StartTrace(VOID);
STATIC bTraceNext = FALSE;
#endif

#if 0                           /* Very suspicious, passing structure by value??
                                   Deactivated -- 2000/06/16 ska */
/* Special entry for far call into the kernel                           */
#pragma argsused
VOID FAR int21_entry(iregs UserRegs)
{
  int21_handler(UserRegs);
}
#endif

/* Structures needed for int 25 / int 26 */
struct HugeSectorBlock {
  ULONG blkno;
  WORD nblks;
  BYTE FAR *buf;
};

/* Normal entry.  This minimizes user stack usage by avoiding local     */
/* variables needed for the rest of the handler.                        */
/* this here works on the users stack !! and only very few functions
   are allowed                                                          */

/* TODO: really, really make sure that this function works properly     */
/* even when SS != DGROUP (some changes to the compiler (!) may be      */
/* necessary).  Currently, between Turbo C++, gcc-ia16, & Open Watcom,  */
/* it seems only Watcom has explicit support for calling near-data      */
/* functions with SS != DGROUP.  The code for this function happens to  */
/* to compile under gcc-ia16 to correctly-behaving code _for now_.  But */
/* it will be better to be able to guarantee this.   -- tkchia 20191207 */

/* Update: I added experimental SS != DGROUP support, and a __seg_ss    */
/* address space qualifier, to gcc-ia16 around January 2020.  This      */
/* function now uses these if available.  In particular, the iregs *irp */
/* structure will always be on the stack, so (as far as the calling     */
/* convention permits) the function can treat the irp pointer as a      */
/* 16-bit pointer relative to SS.                    -- tkchia 20200417 */
#if defined __GNUC__ && defined  __IA16_FEATURE_ATTRIBUTE_NO_ASSUME_SS_DATA
__attribute__((no_assume_ss_data))
VOID ASMCFUNC int21_syscall(iregs __seg_ss * irp, ...)
#else
VOID ASMCFUNC int21_syscall(iregs FAR * irp)
#endif
{
  Int21AX = irp->AX;

  switch (irp->AH)
  {
    /* Set Interrupt Vector - now implemented in entry.asm          */
#if 0
    case 0x25:
      setvec(irp->AL, (intvec)MK_FP(irp->DS, irp->DX));
      break;
#endif

      /* DosVars - get/set dos variables                              */
    case 0x33:
      switch (irp->AL)
      {
          /* Set Ctrl-C flag; returns dl = break_ena              */
        case 0x01:
          break_ena = irp->DL & 1;
          /* fall through so DL only low bit (as in MS-DOS) */

          /* Get Ctrl-C flag                                      */
        case 0x00:
          irp->DL = break_ena;
          break;

        case 0x02:             /* andrew schulman: get/set extended control break  */
          {
            UBYTE tmp = break_ena;
            break_ena = irp->DL & 1;
            irp->DL = tmp;
          }
          break;

          /* Get Boot Drive                                       */
        case 0x05:
          irp->DL = BootDrive;
          break;

          /* Get (real) DOS-C version                             */
        case 0x06:
          irp->BL = os_major;
          irp->BH = os_minor;
          irp->DL = 0;                  /* lower 3 bits revision #, remaining should be 0 */
          irp->DH = version_flags;      /* bit3:runs in ROM,bit 4: runs in HMA */
          break;

     /* case 0x03: */          /* DOS 7 does not set AL */
     /* case 0x07: */          /* neither here */

        default:               /* set AL=0xFF as error, NOT carry */
          irp->AL = 0xff;
          break;

        /* the remaining are FreeDOS extensions */

                        /* return CPU family */
        case 0xfa:
          irp->AL = CPULevel;
          break;

#if 0           /* unknown if used / usage */
                case 0xfb:
#endif

#if 1      /* duplicates DOS 4 int 2F/122Fh, but used by CALLVER */
           /* set FreeDOS returned version for int 21.30 from BX */
        case 0xfc:
          os_setver_major = irp->BL;
          os_setver_minor = irp->BH;
          break;
#endif

          /* Toggle DOS-C rdwrblock trace dump                    */
#ifdef DEBUG
        case 0xfd:
          bDumpRdWrParms = !bDumpRdWrParms;
          break;
#endif

          /* Toggle DOS-C syscall trace dump                      */
#ifdef DEBUG
        case 0xfe:
          bDumpRegs = !bDumpRegs;
          irp->AX = bDumpRegs;
          break;
#endif

          /* Get DOS-C release string pointer                     */
        case 0xff:
#if !defined __GNUC__ || defined  __IA16_FEATURE_ATTRIBUTE_NO_ASSUME_SS_DATA
          irp->DX = FP_SEG(os_release);
#else  /* TODO: remove this hacky SS != DGROUP workaround  --tkchia 20191207 */
          asm volatile("movw %%ds, %0" : "=g" (irp->DX));
#endif
          irp->AX = FP_OFF(os_release);
      }
      break;

    /* Get Interrupt Vector - now implemented in entry.asm            */
#if 0
    case 0x35:
    {
      intvec p = getvec(irp->AL);
      irp->ES = FP_SEG(p);
      irp->BX = FP_OFF(p);
      break;
    }
#endif

      /* Set PSP                                                      */
    case 0x50:
      cu_psp = irp->BX;
      break;

      /* Get PSP                                                      */
    case 0x51:
      /* UNDOCUMENTED: return current psp                             */
    case 0x62:
      irp->BX = cu_psp;

      /* Normal DOS function - DO NOT ARRIVE HERE          */
 /* default: */
  }
}

#ifdef WITHFAT32
      /* DOS 7.0+ FAT32 extended functions */
int int21_fat32(lregs *r)
{
  COUNT rc;

  switch (r->AL)
  {
    /* Get extended drive parameter block */
    case 0x02:
    {
      struct dpb FAR *dpb;
      struct xdpbdata FAR *xddp;

      if (r->CX < sizeof(struct xdpbdata))
        return DE_INVLDBUF;

      dpb = GetDriveDPB(r->DL, &rc);
      if (rc != SUCCESS)
        return rc;

      /* hazard: no error checking! */
      flush_buffers(dpb->dpb_unit);
      dpb->dpb_flags = M_CHANGED;       /* force reread of drive BPB/DPB */

      if (media_check(dpb) < 0)
        return DE_INVLDDRV;

      xddp = MK_FP(r->ES, r->DI);

      fmemcpy(&xddp->xdd_dpb, dpb, sizeof(struct dpb));
      xddp->xdd_dpbsize = sizeof(struct dpb);

      /* if it doesn't look like an extended DPB, fill in those fields */
      if (!ISFAT32(dpb) && dpb->dpb_xsize != dpb->dpb_size)
      {
        xddp->xdd_dpb.dpb_nfreeclst_un.dpb_nfreeclst_st.dpb_nfreeclst_hi =
          (dpb->dpb_nfreeclst == 0xFFFF ? 0xFFFF : 0);
        dpb16to32(&xddp->xdd_dpb);
        xddp->xdd_dpb.dpb_xfatsize = dpb->dpb_fatsize;
        xddp->xdd_dpb.dpb_xcluster = (dpb->dpb_cluster == 0xFFFF ?
                       0xFFFFFFFFuL : dpb->dpb_cluster);
      }
      break;
    }
    /* Get extended free drive space */
    case 0x03:
    {
      struct xfreespace FAR *xfsp = MK_FP(r->ES, r->DI);

      if (r->CX < sizeof(struct xfreespace))
        return DE_INVLDBUF;

      rc = DosGetExtFree(MK_FP(r->DS, r->DX), xfsp);
      if (rc != SUCCESS)
        return rc;
      break;
    }
    /* Set DPB to use for formatting */
    case 0x04:
    {
      struct xdpbforformat FAR *xdffp = MK_FP(r->ES, r->DI);
      struct dpb FAR *dpb;
      if (r->CX < sizeof(struct xdpbforformat))
      {
        return DE_INVLDBUF;
      }
      dpb = GetDriveDPB(r->DL, &rc);
      if (rc != SUCCESS)
        return rc;

      xdffp->xdff_datasize = sizeof(struct xdpbforformat);
      xdffp->xdff_version.actual = 0;

      switch ((UWORD) xdffp->xdff_function)
      {
        case 0x00:
        {
          ULONG nfreeclst = xdffp->xdff_f.setdpbcounts.nfreeclst;
          ULONG cluster = xdffp->xdff_f.setdpbcounts.cluster;
          if (ISFAT32(dpb))
          {
            if ((dpb->dpb_xfsinfosec == 0xffff
                 && (nfreeclst != 0 || cluster != 0))
                || nfreeclst == 1 || nfreeclst > dpb->dpb_xsize
                || cluster == 1 || cluster > dpb->dpb_xsize)
            {
              return DE_INVLDPARM;
            }
            dpb->dpb_xnfreeclst = nfreeclst;
            dpb->dpb_xcluster = cluster;
            write_fsinfo(dpb);
          }
          else
          {
            if ((unsigned)nfreeclst == 1 || (unsigned)nfreeclst > dpb->dpb_size ||
                (unsigned)cluster == 1 || (unsigned)cluster > dpb->dpb_size)
            {
              return DE_INVLDPARM;
            }
            dpb->dpb_nfreeclst = (UWORD)nfreeclst;
            dpb->dpb_cluster = (UWORD)cluster;
          }
          break;
        }
        case 0x01:
        {
          ddt *pddt = getddt(r->DL);
          fmemcpy(&pddt->ddt_bpb, xdffp->xdff_f.rebuilddpb.bpbp,
                  sizeof(bpb));
        }
        case 0x02:
        {
        rebuild_dpb:
            /* hazard: no error checking! */
          flush_buffers(dpb->dpb_unit);
          dpb->dpb_flags = M_CHANGED;

          if (media_check(dpb) < 0)
            return DE_INVLDDRV;
          break;
        }
        case 0x03:
        case 0x04:
        {
          ULONG value;
          if (!ISFAT32(dpb))
            return DE_INVLDPARM;

          value = xdffp->xdff_f.setget.new;
          if ((UWORD) xdffp->xdff_function == 0x03)
          {
            /* FAT mirroring */
            if (value != 0xFFFFFFFFUL && (value & ~(0xf | 0x80)))
                return DE_INVLDPARM;
              xdffp->xdff_f.setget.old = dpb->dpb_xflags;
          }
          else
          {
            /* root cluster */
            if (value != 0xFFFFFFFFUL && (value < 2 || value > dpb->dpb_xsize))
              return DE_INVLDPARM;
            xdffp->xdff_f.setget.old = dpb->dpb_xrootclst;
          }
          if (value != 0xFFFFFFFFUL)
          {
            bpb FAR *bpbp;
            struct buffer FAR *bp = getblock(1, dpb->dpb_unit);
            bp->b_flag &= ~(BFR_DATA | BFR_DIR | BFR_FAT);
            bp->b_flag |= BFR_VALID | BFR_DIRTY;
            bpbp = (bpb FAR *) & bp->b_buffer[BT_BPB];
            if ((UWORD) xdffp->xdff_function == 0x03)
              bpbp->bpb_xflags = (UWORD)value;
            else
              bpbp->bpb_xrootclst = value;
          }
          goto rebuild_dpb;
        }
        default:
          return DE_INVLDFUNC;
      }

      break;
    }
    /* Extended absolute disk read/write */
    /* TODO(vlp) consider using of the 13-14th bits of SI */
    case 0x05:
    {
      struct HugeSectorBlock FAR *SectorBlock =
        (struct HugeSectorBlock FAR *)MK_FP(r->DS, r->BX);
      UBYTE mode;
      /* bit 0 of SI is 0 read / 1 write, bits 13/14 indicate a type:  */
      /* 0 any, 1 fat, 2 dir, 3 file. Type is mostly for "write hints" */

      if (r->CX != 0xffff || (r->SI & ~0x6001))
      {
        return DE_INVLDPARM;
      }

      if (r->DL > lastdrive || r->DL == 0)
        return -0x207;

      if ((r->SI & 1) == 0) /* while uncommon, reads CAN have type hints */
        mode = DSKREADINT25;
      else
        mode = DSKWRITEINT26;

      r->AX =
        dskxfer(r->DL - 1, SectorBlock->blkno, SectorBlock->buf,
                SectorBlock->nblks, mode);

      if (mode == DSKWRITEINT26)
        if (r->AX == 0)
          setinvld(r->DL - 1);

      if (r->AX > 0)
        return -0x20c;
      break;
    }
  default:
    return DE_INVLDFUNC;
  }
  return SUCCESS;
}
#endif

VOID ASMCFUNC int21_service(iregs FAR * r)
{
  COUNT rc;
  long lrc;
  lregs lr; /* 8 local registers (ax, bx, cx, dx, si, di, ds, es) */
  psp FAR *psp = MK_FP(cu_psp, 0);

#define FP_DS_DX (MK_FP(lr.DS, lr.DX))
#define FP_ES_DI (MK_FP(lr.ES, lr.DI))

#define CLEAR_CARRY_FLAG()  r->FLAGS &= ~FLG_CARRY
#define SET_CARRY_FLAG()    r->FLAGS |= FLG_CARRY

  psp->ps_stack = (BYTE FAR *) r;

  fmemcpy(&lr, r, sizeof(lregs) - 4);
  lr.DS = r->DS;
  lr.ES = r->ES;

dispatch:

#ifdef DEBUG
  if (bDumpRegs)
  {
    fmemcpy(&error_regs, user_r, sizeof(iregs));
    DebugPrintf(("System call (21h): %02x\n", user_r->AX));
    dump_regs = TRUE;
    dump();
  }
#endif

  if ((lr.AH >= 0x38 && lr.AH <= 0x4F) || (lr.AH >= 0x56 && lr.AH <= 0x5c) ||
      (lr.AH >= 0x5e && lr.AH <= 0x60) || (lr.AH >= 0x65 && lr.AH <= 0x6a) ||
      lr.AH == 0x6c)
  {
    CLEAR_CARRY_FLAG();
    if (lr.AH != 0x59)
      CritErrCode = SUCCESS;
  }
  /* Clear carry by default for these functions */

  /*
     what happened:
     Application does FindFirst("I:\*.*");
     this fails, and causes Int24
     this sets ErrorMode, and calls Int24
     Application decides NOT to return to DOS,
     but instead pop the stack and return to itself
     (this is legal; see RBIL/INT 24 description

     *) errormode NEVER gets set back to 0 until exit()

     I have NO idea how real DOS handles this;
     the appended patch cures the worst symptoms
  */
  if (/*ErrorMode && */lr.AH > 0x0c && lr.AH != 0x30 && lr.AH != 0x59)
  {
    /*if (ErrorMode)*/ ErrorMode = 0;
  }
  /* Check for Ctrl-Break */
  if (break_ena || (lr.AH >= 1 && lr.AH <= 5) || (lr.AH >= 8 && lr.AH <= 0x0b))
    check_handle_break(&syscon);

  /* The dispatch handler                                         */
  switch (lr.AH)
  {
      /* int 21h common error handler                                 */
    case 0x64:
        goto error_invalid;

      /* case 0x00:   --> Simulate a DOS-4C-00 */

      /* Read Keyboard with Echo                      */
    case 0x01:
  DOS_01:
      lr.AL = read_char_stdin(TRUE);
      write_char_stdout(lr.AL);
      break;

      /* Display Character                                            */
    case 0x02:
  DOS_02:
      lr.AL = lr.DL;
      write_char_stdout(lr.AL);
      break;

      /* Auxiliary Input                                                      */
    case 0x03:
    {
      int sft_idx = get_sft_idx(STDAUX);
      lr.AL = read_char(sft_idx, sft_idx, TRUE);
      break;
    }

      /* Auxiliary Output                                                     */
    case 0x04:
      write_char(lr.DL, get_sft_idx(STDAUX));
      break;
      /* Print Character                                                      */
    case 0x05:
      write_char(lr.DL, get_sft_idx(STDPRN));
      break;

      /* Direct Console I/O                                            */
    case 0x06:
  DOS_06:
      if (lr.DL != 0xff)
        goto DOS_02;

      lr.AL = 0x00;
      r->FLAGS |= FLG_ZERO;
      if (StdinBusy()) {
        DosIdle_int();
        break;
      }

      r->FLAGS &= ~FLG_ZERO;
      /* fall through */

      /* Direct Console Input                                         */
    case 0x07:
  DOS_07:
      lr.AL = read_char_stdin(FALSE);
      break;

      /* Read Keyboard Without Echo                                   */
    case 0x08:
  DOS_08:
      lr.AL = read_char_stdin(TRUE);
      break;

      /* Display String                                               */
    case 0x09:
      {
        unsigned char c;
        unsigned char FAR *bp = FP_DS_DX;

        while ((c = *bp++) != '$')
          write_char_stdout(c);

        lr.AL = c;
      }
      break;

      /* Buffered Keyboard Input                                      */
    case 0x0a:
  DOS_0A:
      read_line(get_sft_idx(STDIN), get_sft_idx(STDOUT), FP_DS_DX);
      break;

      /* Check Stdin Status                                           */
    case 0x0b:
      lr.AL = 0xFF;
      if (StdinBusy())
        lr.AL = 0x00;
      break;

      /* Flush Buffer, Read Keyboard                                 */
    case 0x0c:
    {
      struct dhdr FAR *dev = sft_to_dev(get_sft(STDIN));
      if (dev)
        con_flush(&dev);
      switch (lr.AL)
      {
      case 0x01: goto DOS_01;
      case 0x06: goto DOS_06;
      case 0x07: goto DOS_07;
      case 0x08: goto DOS_08;
      case 0x0a: goto DOS_0A;
      }
      lr.AL = 0x00;
      break;
    }

      /* Reset Drive                                                  */
    case 0x0d:
      flush();
      break;

      /* Set Default Drive                                            */
    case 0x0e:
      lr.AL = DosSelectDrv(lr.DL);
      break;

    case 0x0f:
      lr.AL = FcbOpen(FP_DS_DX, O_FCB | O_LEGACY | O_OPEN | O_RDWR);
      break;

    case 0x10:
      lr.AL = FcbClose(FP_DS_DX);
      break;

    case 0x11:
      lr.AL = FcbFindFirstNext(FP_DS_DX, TRUE);
      break;

    case 0x12:
      lr.AL = FcbFindFirstNext(FP_DS_DX, FALSE);
      break;

    case 0x13:
      lr.AL = FcbDelete(FP_DS_DX);
      break;

    case 0x14:
      /* FCB read */
      lr.AL = FcbReadWrite(FP_DS_DX, 1, XFR_READ);
      break;

    case 0x15:
      /* FCB write */
      lr.AL = FcbReadWrite(FP_DS_DX, 1, XFR_WRITE);
      break;

    case 0x16:
      lr.AL = FcbOpen(FP_DS_DX, O_FCB | O_LEGACY | O_CREAT | O_TRUNC | O_RDWR);
      break;

    case 0x17:
      lr.AL = FcbRename(FP_DS_DX);
      break;

    default:
#ifdef DEBUG
      printf("Unsupported INT21 AH = 0x%x, AL = 0x%x.\n", lr.AH, lr.AL);
#endif
      /* Fall through. */

      /* CP/M compatibility functions                                 */
    case 0x18:
    case 0x1d:
    case 0x1e:
    case 0x20:
#ifndef TSC
    case 0x61:
#endif
    case 0x6b:
      lr.AL = 0;
      break;

      /* Get Default Drive                                            */
    case 0x19:
      lr.AL = default_drive;
      break;

      /* Set DTA                                                      */
    case 0x1a:
      dta = FP_DS_DX;
      break;

      /* Get Default Drive Data                                       */
    case 0x1b:
      lr.DL = 0;
      /* fall through */

      /* Get Drive Data                                               */
    case 0x1c:
      {
        BYTE FAR *p;

        if ((p = FatGetDrvData(lr.DL, &lr.AL, &lr.CX, &lr.DX)) != NULL)
        {
          lr.DS = FP_SEG(p);
          lr.BX = FP_OFF(p);
        }
        else
          lr.AL = 0xff;  /* return 0xff on invalid drive */
      }
      break;

      /* Get default DPB                                              */
      /* case 0x1f: see case 0x32 */

      /* Random read using FCB: fields not updated
         (XFR_RANDOM should not be used here) */
    case 0x21:
      lr.AL = FcbRandomIO(FP_DS_DX, XFR_READ);
      break;

      /* Random write using FCB */
    case 0x22:
      lr.AL = FcbRandomIO(FP_DS_DX, XFR_WRITE);
      break;

      /* Get file size in records using FCB */
    case 0x23:
      lr.AL = FcbGetFileSize(FP_DS_DX);
      break;

      /* Set random record field in FCB */
    case 0x24:
      FcbSetRandom(FP_DS_DX);
      break;

      /* Set Interrupt Vector                                         */
      /* case 0x25: handled above (re-entrant)                        */

      /* Dos Create New Psp                                           */
    case 0x26:
      new_psp(lr.DX, r->CS);
      break;

      /* Read random record(s) using FCB */
    case 0x27:
      lr.AL = FcbRandomBlockIO(FP_DS_DX, &lr.CX, XFR_READ | XFR_FCB_RANDOM);
      break;

      /* Write random record(s) using FCB */
    case 0x28:
      lr.AL = FcbRandomBlockIO(FP_DS_DX, &lr.CX, XFR_WRITE | XFR_FCB_RANDOM);
      break;

      /* Parse File Name                                              */
    case 0x29:
      lr.SI = FcbParseFname(&lr.AL, MK_FP(lr.DS, lr.SI), FP_ES_DI);
      break;

      /* Get Date                                                     */
    case 0x2a:
      lr.AL = DosGetDate((struct dosdate *)&lr.CX);
      break;

      /* Set Date                                                     */
    case 0x2b:
      lr.AL = DosSetDate ((struct dosdate*)&lr.CX) == SUCCESS ? 0 : 0xFF;
      break;

      /* Get Time                                                     */
    case 0x2c:
      DosGetTime((struct dostime *)&lr.CL);
      break;

      /* Set Time                                                     */
    case 0x2d:
      lr.AL = DosSetTime ((struct dostime*)&lr.CX) == SUCCESS ? 0 : 0xFF;
      break;

      /* Set verify flag                                              */
    case 0x2e:
      verify_ena = lr.AL & 1;
      break;

      /* Get DTA                                                      */
    case 0x2f:
      lr.BX = FP_OFF(dta);
      lr.ES = FP_SEG(dta);
      break;

      /* Get (editable) DOS Version                                   */
    case 0x30:
    {
      if (lr.AL == 1) /* from RBIL, if AL=1 then return version_flags */
          lr.BH = version_flags;
      else
          lr.BH = OEM_ID;
      lr.AX = psp->ps_retdosver;
      lr.BL = REVISION_SEQ;
      lr.CX = 0; /* do not set this to a serial number!
                    32RTM won't like non-zero values   */

      if (ReturnAnyDosVersionExpected)
      {
        /* TE for testing purpose only and NOT
           to be documented:
           return programs, who ask for version == XX.YY
           exactly this XX.YY.
           this makes most MS programs more happy.
         */
        UBYTE FAR *retp = MK_FP(r->cs, r->ip);

        if (retp[0] == 0x3d &&  /* cmp ax, xxyy */
            (retp[3] == 0x75 || retp[3] == 0x74))       /* je/jne error    */
        {
          lr.AL = retp[1];
          lr.AH = retp[2];
        }
        else if (retp[0] == 0x86 &&     /* xchg al,ah   */
                 retp[1] == 0xc4 && retp[2] == 0x3d &&  /* cmp ax, xxyy */
                 (retp[5] == 0x75 || retp[5] == 0x74))  /* je/jne error    */
        {
          lr.AL = retp[4];
          lr.AH = retp[3];
        }

      }
    }

    break;

      /* Keep Program (Terminate and stay resident) */
    case 0x31:
      DosMemChange(cu_psp, lr.DX < 6 ? 6 : lr.DX, 0);
      return_code = lr.AL | 0x300;
      term_type = 3; /* ecm: TSR terminate */
      return_user();
      break;

      /* Get default BPB */
    case 0x1f:
      /* Get DPB                                                      */
    case 0x32:
      /* r->DL is NOT changed by MS 6.22 */
      /* INT21/32 is documented to reread the DPB */
      {
        int drv = (lr.DL == 0 || lr.AH == 0x1f) ? default_drive : lr.DL - 1;
        struct dpb FAR *dpb = get_dpb(drv);

        if (dpb == NULL)
        {
          CritErrCode = -DE_INVLDDRV;
          lr.AL = 0xFF;
          break;
        }
        /* hazard: no error checking! */
        flush_buffers(dpb->dpb_unit);
        dpb->dpb_flags = M_CHANGED;     /* force flush and reread of drive BPB/DPB */

#ifdef WITHFAT32
        if (media_check(dpb) < 0 || ISFAT32(dpb))
#else
        if (media_check(dpb) < 0)
#endif
        {
          lr.AL = 0xff;
          CritErrCode = -DE_INVLDDRV;
          break;
        }
        lr.DS = FP_SEG(dpb);
        lr.BX = FP_OFF(dpb);
        lr.AL = 0;
      }

      break;
/*
    case 0x33:
    see int21_syscall
*/
      /* Get InDOS flag                                               */
    case 0x34:
      lr.BX = FP_OFF(&InDOS);
      lr.ES = FP_SEG(&InDOS);
      break;

      /* Get Interrupt Vector                                         */
      /* case 0x35: handled above (reentrant)                         */

      /* Dos Get Disk Free Space                                      */
    case 0x36:
      lr.AX = DosGetFree(lr.DL, &lr.BX, &lr.CX, &lr.DX);
      break;

      /* Undocumented Get/Set Switchar                                */
    case 0x37:
      switch (lr.AL)
      {
          /* Get switch character */
        case 0x00:
          lr.DL = switchar;
          lr.AL = 0x00;
          break;

          /* Set switch character */
        case 0x01:
          switchar = lr.DL;
          lr.AL = 0x00;
          break;

        default:
          goto error_invalid;
      }
      break;

      /* Get/Set Country Info                                         */
    case 0x38:
      {
        UWORD cntry = lr.AL;

        if (cntry == 0xff)
          cntry = lr.BX;

        if (0xffff == lr.DX)
        {
          /* Set Country Code */
          rc = DosSetCountry(cntry);
        }
        else
        {
          if (cntry == 0)
            cntry--;
          /* Get Country Information */
          rc = DosGetCountryInformation(cntry, FP_DS_DX);
          if (rc >= SUCCESS)
          {
            if (cntry == (UWORD) - 1)
              cntry = nlsInfo.actPkg->cntry;
            lr.AX = lr.BX = cntry;
          }
        }
        goto short_check;
      }

      /* Dos Create Directory                                         */
    case 0x39:
      /* Dos Remove Directory                                         */
    case 0x3a:
      rc = DosMkRmdir(FP_DS_DX, lr.AH);
      goto short_check;

      /* Dos Change Directory                                         */
    case 0x3b:
      rc = DosChangeDir(FP_DS_DX);
      goto short_check;

      /* Dos Create File                                              */
    case 0x3c:
      lrc = DosOpen(FP_DS_DX, O_LEGACY | O_RDWR | O_CREAT | O_TRUNC, lr.CL);
      goto long_check;

      /* Dos Open                                                     */
    case 0x3d:
      lrc = DosOpen(FP_DS_DX, O_LEGACY | O_OPEN | lr.AL, 0);
      goto long_check;

      /* Dos Close                                                    */
    case 0x3e:
      rc = DosClose(lr.BX);
      goto short_check;

      /* Dos Read                                                     */
    case 0x3f:
      lrc = DosRead(lr.BX, lr.CX, FP_DS_DX);
      goto long_check;

      /* Dos Write                                                    */
    case 0x40:
      lrc = DosWrite(lr.BX, lr.CX, FP_DS_DX);
      goto long_check;

      /* Dos Delete File                                              */
    case 0x41:
      rc = DosDelete((BYTE FAR *) FP_DS_DX, D_ALL);
      goto short_check;

      /* Dos Seek                                                     */
    case 0x42:
      if (lr.AL > 2)
        goto error_invalid;
      lrc = DosSeek(lr.BX, (LONG)MK_ULONG(lr.CX, lr.DX), lr.AL, &rc);
      if (rc == SUCCESS)
      {
        lr.DX = (UWORD)(lrc >> 16);
        lr.AX = (UWORD) lrc;
      }
      goto short_check;

      /* Get/Set File Attributes                                      */
    case 0x43:
      switch (lr.AL)
      {
        case 0x00:
          rc = DosGetFattr((BYTE FAR *) FP_DS_DX);
          if (rc >= SUCCESS)
            lr.CX = rc;
          break;

        case 0x01:
          rc = DosSetFattr((BYTE FAR *) FP_DS_DX, lr.CX);
          lr.AX = lr.CX;
          break;

#if 0
        case 0x02:
            /* get compressed size -> compression not support returns size in clusters */
            /* rc = DosGetClusterCnt((BYTE FAR *) FP_DS_DX); */
          goto error_invalid;
#endif

        case 0xff: /* DOS 7.20 (w98) extended name (128 char length) functions */
        {
          switch(lr.CL)
          {
                /* Dos Create Directory                                         */
                case 0x39:
                /* Dos Remove Directory                                         */
                case 0x3a:
                rc = DosMkRmdir(FP_DS_DX, lr.CL);
                goto short_check;

                /* Dos rename file */
                case 0x56:
                rc = DosRename(FP_DS_DX, FP_ES_DI);
                goto short_check;

              /* fall through to goto error_invaid */
          }
        }

        default:
          goto error_invalid;
      }
      goto short_check;

      /* Device I/O Control                                           */
    case 0x44:
      rc = DosDevIOctl(&lr);      /* can set critical error code! */

      if (rc < SUCCESS)
      {
        lr.AX = -rc;
        if (rc != DE_DEVICE && rc != DE_ACCESS)
          CritErrCode = lr.AX;
        goto error_carry;
      }
      break;

      /* Duplicate File Handle                                        */
    case 0x45:
      lrc = DosDup(lr.BX);
      goto long_check;

      /* Force Duplicate File Handle                                  */
    case 0x46:
      rc = DosForceDup(lr.BX, lr.CX);
      goto short_check;

      /* Get Current Directory                                        */
    case 0x47:
      rc = DosGetCuDir(lr.DL, MK_FP(lr.DS, lr.SI));
      lr.AX = 0x0100;         /*jpp: from interrupt list */
      goto short_check;

      /* Allocate memory */
    case 0x48:
      if ((rc = DosMemAlloc(lr.BX, mem_access_mode, &lr.AX, &lr.BX)) < 0)
      {
        DosMemLargest(&lr.BX);
        if (DosMemCheck() != SUCCESS)
          panic("MCB chain corrupted");
        goto error_exit;
      }
      lr.AX++;   /* DosMemAlloc() returns seg of MCB rather than data */
      break;

      /* Free memory */
    case 0x49:
      if ((rc = DosMemFree(lr.ES - 1)) < SUCCESS)
      {
        if (DosMemCheck() != SUCCESS)
          panic("MCB chain corrupted");
        goto error_exit;
      }
      break;

      /* Set memory block size */
    case 0x4a:
        if (DosMemCheck() != SUCCESS)
          panic("before 4a: MCB chain corrupted");

      if ((rc = DosMemChange(lr.ES, lr.BX, &lr.BX)) < 0)
      {
#if 0
        if (cu_psp == lr.ES)
        {
          psp->ps_size = lr.BX + cu_psp;
        }
#endif
        if (DosMemCheck() != SUCCESS)
          panic("after 4a: MCB chain corrupted");
        goto error_exit;
      }
      lr.AX = lr.ES; /* Undocumented MS-DOS behaviour expected by BRUN45! */
      break;

      /* Load and Execute Program */
    case 0x4b:
      break_flg = FALSE;

      rc = DosExec(lr.AL, MK_FP(lr.ES, lr.BX), FP_DS_DX);
      goto short_check;

      /* Terminate Program                                            */
    case 0x00:
      lr.AX = 0x4c00;

      /* End Program                                                  */
    case 0x4c:
      if (break_flg) {
        break_flg = FALSE;
        term_type = 1;
      }
      rc = term_type << 8;
      return_code = lr.AL | rc;
      if (DosMemCheck() != SUCCESS)
        panic("MCB chain corrupted");
#ifdef TSC
      StartTrace();
#endif
      return_user();
      break;

      /* Get Child-program Return Value                               */
    case 0x4d:
      lr.AX = return_code;
      /* needs to be cleared (RBIL) */
      return_code = 0;
      break;

      /* Dos Find First                                               */
    case 0x4e:
      /* dta for this call is set on entry.  This     */
      /* needs to be changed for new versions.        */
      rc = DosFindFirst(lr.CX, FP_DS_DX);
      lr.AX = 0;
      goto short_check;

      /* Dos Find Next                                                */
    case 0x4f:
      /* dta for this call is set on entry.  This     */
      /* needs to be changed for new versions.        */
      rc = DosFindNext();
      lr.AX = 0;
      goto short_check;
/*
    case 0x50:
    case 0x51:
    see int21_syscall
*/
      /* ************UNDOCUMENTED************************************* */
      /* Get List of Lists                                            */
    case 0x52:
      lr.BX = FP_OFF(&DPBp);
      lr.ES = FP_SEG(&DPBp);
      break;

    case 0x53:
      /*  DOS 2+ internal - TRANSLATE BIOS PARAMETER BLOCK TO DRIVE PARAM BLOCK */
      bpb_to_dpb((bpb FAR *) MK_FP(lr.DS, lr.SI),
                 (struct dpb FAR *)MK_FP(lr.ES, r->BP)
#ifdef WITHFAT32
                 , (lr.CX == 0x4558 && lr.DX == 0x4152)
#endif
          );
      break;

      /* Get verify state                                             */
    case 0x54:
      lr.AL = verify_ena;
      break;

      /* ************UNDOCUMENTED************************************* */
      /* Dos Create New Psp & set p_size                              */
    case 0x55:
      child_psp(lr.DX, cu_psp, lr.SI);
      /* copy command line from the parent (required for some device loaders) */
      fmemcpy(MK_FP(lr.DX, 0x80), MK_FP(cu_psp, 0x80), 128);
      cu_psp = lr.DX;
      break;

      /* Dos Rename                                                   */
    case 0x56:
      rc = DosRename(FP_DS_DX, FP_ES_DI);
      goto short_check;

      /* Get/Set File Date and Time                                   */
    case 0x57:
      switch (lr.AL)
      {
        case 0x00:
          rc = DosGetFtime((COUNT) lr.BX,       /* Handle               */
                           &lr.DX,        /* FileDate             */
                           &lr.CX);       /* FileTime             */
          break;

        case 0x01:
          rc = DosSetFtime((COUNT) lr.BX,       /* Handle               */
                           (ddate) lr.DX,       /* FileDate             */
                           (dtime) lr.CX);      /* FileTime             */
          break;

        default:
          goto error_invalid;
      }
      goto short_check;

      /* Get/Set Allocation Strategy                                  */
    case 0x58:
      switch (lr.AL)
      {
        case 0x00:
          lr.AL = mem_access_mode;
          lr.AH = 0;
          break;

        case 0x01:
          if (lr.BL > LAST_FIT_U ||                 /* 0x82       */
              (lr.BL & FIT_MASK) > LAST_FIT)        /* 0x3f, 0x02 */
            goto error_invalid;
          mem_access_mode = lr.BL;
          break;

        case 0x02:
          lr.AL = uppermem_link;
          break;

        case 0x03:
          if (uppermem_root != 0xffff)    /* always error if not exists */
          {
            DosUmbLink(lr.BX);
            break;
          }
          /* else fall through */

        default:
          goto error_invalid;
#ifdef DEBUG
        case 0xff:
          show_chain();
          break;
#endif
      }
      break;

      /* Get Extended Error */
    case 0x59:
      lr.AX = CritErrCode;
      lr.CH = CritErrLocus;
      lr.BH = CritErrClass;
      lr.BL = CritErrAction;
      lr.DI = FP_OFF(CritErrDev);
      lr.ES = FP_SEG(CritErrDev);
      break;

      /* Create Temporary File */
    case 0x5a:
      lrc = DosMkTmp(FP_DS_DX, lr.CX);
      goto long_check;

      /* Create New File */
    case 0x5b:
      lrc = DosOpen(FP_DS_DX, O_LEGACY | O_RDWR | O_CREAT, lr.CX);
      goto long_check;

/* /// Added for SHARE.  - Ron Cemer */
      /* Lock/unlock file access */
    case 0x5c:
      rc = DosLockUnlock
           (lr.BX, ((unsigned long)lr.CX << 16) | lr.DX,
                   ((unsigned long)lr.SI << 16) | lr.DI, lr.AL != 0);
      if (rc != SUCCESS)
        goto error_exit;
      break;
/* /// End of additions for SHARE.  - Ron Cemer */

      /* UNDOCUMENTED: server, share.exe and sda function             */
    case 0x5d:
      switch (lr.AL)
      {
          /* Remote Server Call */
        case 0x00:
          fmemcpy(&lr, FP_DS_DX, sizeof(lr));
          goto dispatch;

        case 0x06:
          lr.DS = FP_SEG(internal_data);
          lr.SI = FP_OFF(internal_data);
          lr.CX = swap_indos - internal_data;
          lr.DX = swap_always - internal_data;
          CLEAR_CARRY_FLAG();
          break;

        case 0x07:
        case 0x08:
        case 0x09:
          rc = remote_printredir(lr.DX, Int21AX);
          CLEAR_CARRY_FLAG();
          if (rc != SUCCESS)
            goto error_exit;
          break;

          /* Set Extended Error */
        case 0x0a:
          {
            lregs far *er      = FP_DS_DX;
            CritErrCode        = er->AX;
            CritErrDev         = MK_FP(er->ES, er->DI);
            CritErrLocus       = er->CH;
            CritErrClass       = er->BH;
            CritErrAction      = er->BL;
            CLEAR_CARRY_FLAG();
            break;
          }

        default:
          CritErrCode = SUCCESS;
          goto error_invalid;
      }
      break;

    case 0x5e:
      switch (lr.AL)
      {
        case 0x00:
          lr.CX = get_machine_name(FP_DS_DX);
          break;

        case 0x01:
          set_machine_name(FP_DS_DX, lr.CX);
          break;

        default:
          rc = (int)network_redirector_mx(REM_PRINTSET, &lr, (void *)Int21AX);
          goto short_check;
      }
      break;

    case 0x5f:
      if (lr.AL == 7 || lr.AL == 8)
      {
        if (lr.DL < lastdrive)
        {
          struct cds FAR *cdsp = CDSp + lr.DL;
          if (FP_OFF(cdsp->cdsDpb))     /* letter of physical drive?    */
          {
            cdsp->cdsFlags &= ~CDSPHYSDRV;
            if (lr.AL == 7)
              cdsp->cdsFlags |= CDSPHYSDRV;
            break;
          }
        }
        rc = DE_INVLDDRV;
        goto error_exit;
      }
      else
      {
        rc = (int)network_redirector_mx(REM_DOREDIRECT, &lr, (void *)Int21AX);
        /* the remote function manipulates *r directly !,
           so we should not copy lr to r here            */
        if (rc != SUCCESS)
        {
          CritErrCode = -rc;      /* Maybe set */
          SET_CARRY_FLAG();
        }
        r->AX = -rc;            /* okay because we use real_exit */
        goto real_exit;
      }

    case 0x60:                 /* TRUENAME */
      rc = DosTruename(MK_FP(lr.DS, lr.SI), adjust_far(FP_ES_DI));
      lr.AX = rc;
      goto short_check;

#ifdef TSC
      /* UNDOCUMENTED: no-op                                          */
      /*                                                              */
      /* DOS-C: tsc support                                           */
    case 0x61:
#ifdef DEBUG
      switch (lr.AL)
      {
        case 0x01:
          bTraceNext = TRUE;
          break;

        case 0x02:
          bDumpRegs = FALSE;
          break;
      }
#endif
      lr.AL = 0x00;
      break;
#endif

      /* UNDOCUMENTED: return current psp
         case 0x62: is in int21_syscall
         lr.BX = cu_psp;
         break;
       */

      /* UNDOCUMENTED: Double byte and korean tables                  */
    case 0x63:
      {
        VOID FAR *p;
#if 0
        /* not really supported, but will pass.                 */
        lr.AL = 0x00;           /*jpp: according to interrupt list */
        /*Bart: fails for PQDI and WATCOM utilities:
           use the above again */
#endif
        switch (lr.AL)
        {
          case 0:
            p = DosGetDBCS();
            lr.DS = FP_SEG(p);
            lr.SI = FP_OFF(p) + 2;
            break;
          case 1: /* set Korean Hangul input method to DL 0/1 */
            lr.AL = 0xff;       /* flag error (AL would be 0 if okay) */
            break;
          case 2: /* get Korean Hangul input method setting to DL */
            lr.AL = 0xff;       /* flag error, do not set DL */
            break;
          default:      /* is this the proper way to handle invalid AL? */
            rc = -1;
            goto error_exit;
        }
        break;
      }
/*
    case 0x64:
      see above (invalid)
*/

      /* Extended country info                                        */
    case 0x65:
      switch (lr.AL)
      {
        case 0x20:             /* upcase single character */
          lr.DL = DosUpChar(lr.DL);
          break;
        case 0x21:             /* upcase memory area */
          DosUpMem(FP_DS_DX, lr.CX);
          break;
        case 0x22:             /* upcase ASCIZ */
          DosUpString(FP_DS_DX);
          break;
        case 0xA0:             /* upcase single character of filenames */
          lr.DL = DosUpFChar(lr.DL);
          break;
        case 0xA1:             /* upcase memory area of filenames */
          DosUpFMem(FP_DS_DX, lr.CX);
          break;
        case 0xA2:             /* upcase ASCIZ of filenames */
          DosUpFString(FP_DS_DX);
          break;
        case 0x23:             /* check Yes/No response */
          lr.AX = DosYesNo(lr.DL);
          break;
        default:
#ifdef NLS_DEBUG
          if ((rc = DosGetData(lr.AL, lr.BX, lr.DX, lr.CX, FP_ES_DI)) < 0)
          {
            printf("DosGetData() := %d\n", rc);
            goto error_exit;
          }
          printf("DosGetData() returned successfully\n");
          break;
#else
          rc = DosGetData(lr.AL, lr.BX, lr.DX, lr.CX, FP_ES_DI);
          goto short_check;
#endif
      }
      break;

      /* Code Page functions */
    case 0x66:
        switch (lr.AL)
        {
          case 1:
            rc = DosGetCodepage(&lr.BX, &lr.DX);
            break;
          case 2:
            rc = DosSetCodepage(lr.BX, lr.DX);
            break;

          default:
            goto error_invalid;
        }
        if (rc != SUCCESS)
          goto error_exit;
        break;

      /* Set Max file handle count */
    case 0x67:
      rc = SetJFTSize(lr.BX);
      goto short_check;

      /* Flush file buffer -- COMMIT FILE.  */
    case 0x68:
    case 0x6a:
      rc = DosCommit(lr.BX);
      goto short_check;

      /* Get/Set Serial Number */
    case 0x69:
      rc = (lr.BL == 0 ? default_drive : lr.BL - 1);
      if (lr.AL < 2)
      {
        if (get_cds(rc) == NULL)
        {
          rc = DE_INVLDDRV;
          goto error_exit;
        }
        if (get_dpb(rc) != NULL)
        {
          UWORD saveCX = lr.CX;
          lr.CX = lr.AL == 0 ? 0x0866 : 0x0846;
          lr.AL = 0x0d;
          rc = DosDevIOctl(&lr);
          lr.CX = saveCX;
          goto short_check;
        }
      }
      goto error_invalid;
/*
    case 0x6a: see case 0x68
    case 0x6b: dummy func: return AL=0
*/
      /* Extended Open-Creat, not fully functional. (bits 4,5,6 of BH) */
    case 0x6c:
      /* high nibble must be <= 1, low nibble must be <= 2 */
      if ((lr.DL & 0xef) > 0x2)
        goto error_invalid;
      lrc = DosOpen(MK_FP(lr.DS, lr.SI),
                    (lr.BX & 0x70ff) | ((lr.DL & 3) << 8) |
                    ((lr.DL & 0x10) << 6), lr.CL);
      if (lrc >= SUCCESS)
        /* action */
        lr.CX = (UWORD)(lrc >> 16);
      goto long_check;

      /* case 0x6d and above not implemented : see default; return AL=0 */

#ifdef WITHFAT32
    /* LFN API */
    case 0x71:
      switch (lr.AL)
      {
#ifdef WITHLFNAPI
        /* Win95 LFN - reset drive */
        case 0x0d:
        /* Win95 LFN - make directory */
        case 0x39:
        /* Win95 LFN - remove directory */
        case 0x3a:
        /* Win95 LFN - change directory */
        case 0x3b:
        /* Win95 LFN - delete file */
        case 0x41:
        /* Win95 LFN - extended get/set file attributes */
        case 0x43:
        /* Win95 LFN - get current directory */
        case 0x47:
        /* Win95 LFN - find first file */
        case 0x4e:
        /* Win95 LFN - find next file */
        case 0xa2:  /* internal - same as 0x4f */
        case 0x4f: {
          goto unsupp;
        }
        /* Win95 LFN - rename file */
        case 0x56:
        /* Win95 LFN - canonicalize file name/path */
        case 0x60: {
          switch (lr.CL)
          {
            /* truename - canonicalize path, accepts short/long/or combination as input and may return combined short/long name */
            case 0x00: {
            }
            /* get canonical short (8.3) name or path, truename that accepts long name and returns short name  */
            case 0x01: {
            }
            /* get canonical long name or path, truename that accepts short name and returns long name */
            case 0x02: {
            }
            default:
              goto unsupp;
          }
        }
        /* Win95 LFN - create or open file */
        case 0xa9:  /* for real-mode servers only, AX is _global_ file handle on return */
        case 0x6c: {
          goto unsupp;
        }
        /* Win95 LFN - get volume information */
        case 0xa0:
        /* Win95 LFN - find file close */
        case 0xa1:  {
lfn_findclose:
          goto unsupp;
        }
#if 0
        /* Win95 LFN - internal use ??? */
        case 0xa3:
        case 0xa4:
        case 0xa5:
          goto unsupp;
#endif
#endif
        /* EDR-DOS LFN - Long LSEEK - SET CURRENT 64-bit FILE POSITION */
        case 0x42:
        /* Win95 LFN - get file info by handle */
        case 0xa6: {
          /* only passed to redirector supported for now */
          iregs saved_r;
          sft FAR *s;
          unsigned char idx;

          if (lr.BX >= psp->ps_maxfiles)
          {
            rc = DE_INVLDHNDL;
            goto error_exit;
          }
          idx = psp->ps_filetab[lr.BX];
          s = idx_to_sft(idx);
          if (s == (sft FAR *)-1)
          {
            rc = DE_INVLDHNDL;
            goto error_exit;
          }
          if (!(s->sft_flags & SFT_FSHARED)) {
            if (lr.AL != 0x42) {
              goto unsupp;    /* unsupported on local fs yet */
            }
          }
          /* call to redirector */
          fmemcpy(&saved_r, r, sizeof(saved_r));
          r->ES = FP_SEG(s);
          r->DI = FP_OFF(s);
          r->flags |= FLG_CARRY;
          r->AH = 0x11;
          call_intr(0x2f, r);
          if (!(r->flags & FLG_CARRY)) {
            r->ES = saved_r.ES;
            r->DI = saved_r.DI;
            goto real_exit;
          }
          /* carry still set - unhandled */
          fmemcpy(r, &saved_r, sizeof(saved_r));
          goto unsupp;
        }
#ifdef WITHLFNAPI
        /* Win95 LFN - Win95 64 UTC file time to/from DOS date and time (local timezone) */
        case 0xa7: {
          /* Note: valid input range limited to Jan 1, 1980 to Dec 31, 2107 */
          switch (lr.BL)
          {
            /* from Win95 UTC to DOS date/time */
            case 0x00: {
            }
            /* from DOS date/time to Win95 UTC */
            case 0x01: {
            }
            default:
              goto unsupp;
          }
        }
        /* Win95 LFN - generate short filename */
        case 0xa8:
        /* Win95 LFN - subst */
        case 0xaa: {
          goto unsupp;
        }
#endif
        default:
          goto unsupp;
      }
#ifdef WITHLFNAPI
    /* Win95 beta LFN - find close */
    case 0x72: goto lfn_findclose;
#endif

      /* DOS 7.0+ FAT32 extended functions */
    case 0x73:
      CLEAR_CARRY_FLAG();
      CritErrCode = SUCCESS;
      rc = int21_fat32(&lr);
      goto short_check;
#endif

#ifdef WITHLFNAPI
      /* FreeDOS LFN helper API functions */
    case 0x74:
      {
        switch (lr.AL)
        {
          /* Allocate LFN inode */
          case 0x01:
            rc = lfn_allocate_inode();
            break;
          /* Free LFN inode */
          case 0x02:
            rc = lfn_free_inode(lr.BX);
            break;
          /* Setup LFN inode */
          case 0x03:
            rc = lfn_setup_inode(lr.BX, MK_ULONG(lr.CX, lr.DX), MK_ULONG(lr.SI,lr.DI));
            break;
          /* Create LFN entries */
          case 0x04:
            rc = lfn_create_entries(lr.BX, (lfn_inode_ptr)FP_DS_DX);
            break;
          /* Read next LFN */
          case 0x05:
            rc = lfn_dir_read(lr.BX, (lfn_inode_ptr)FP_DS_DX);
            break;
          /* Write SFN pointed by LFN inode */
          case 0x06:
            rc = lfn_dir_write(lr.BX);
            break;
          default:
            goto error_invalid;
        }
        lr.AX = rc;
        CLEAR_CARRY_FLAG();
        goto short_check;
      }
#endif
  }
  goto exit_dispatch;
#ifdef WITHFAT32
unsupp:
  {
    lr.AL = 00;
    goto error_carry;
  }
#endif
long_check:
  if (lrc >= SUCCESS)
  {
    lr.AX = (UWORD)lrc;
    goto exit_dispatch;
  }
  rc = (int)lrc;
short_check:
  if (rc < SUCCESS)
    goto error_exit;
  goto exit_dispatch;
error_invalid:
  rc = DE_INVLDFUNC;
error_exit:
  lr.AX = -rc;
  if (CritErrCode == SUCCESS)
    CritErrCode = lr.AX;      /* Maybe set */
error_carry:
  SET_CARRY_FLAG();
exit_dispatch:
  fmemcpy(r, &lr, sizeof(lregs) - 4); /* copy lr -> r but exclude flags */
  r->DS = lr.DS;
  r->ES = lr.ES;
real_exit:;

#ifdef DEBUG
  if (bDumpRegs)
  {
    fmemcpy(&error_regs, user_r, sizeof(iregs));
    dump_regs = TRUE;
    dump();
  }
#endif
}

#if 0
        /* No kernel INT-23 handler required no longer -- 1999/04/15 ska */
/* ctrl-Break handler */
#pragma argsused
VOID INRPT FAR int23_handler(int es, int ds, int di, int si, int bp,
                             int sp, int bx, int dx, int cx, int ax,
                             int ip, int cs, int flags)
{
  term_type = 1;
  return_mode = 1;
  return_code = -1;
  mod_sto(CTL_C);
  DosMemCheck();
#ifdef TSC
  StartTrace();
#endif
  return_user();
}
#endif

struct int25regs {
  UWORD es, ds;
  UWORD di, si, bp, sp;
  UWORD bx, dx, cx, ax;
  UWORD flags, ip, cs;
};

/*
    this function is called from an assembler wrapper function
*/
VOID ASMCFUNC int2526_handler(WORD mode, struct int25regs FAR * r)
{
  ULONG blkno;
  UWORD nblks;
  BYTE FAR *buf;
  UBYTE drv;
  struct dpb FAR *dpbp;

  if (mode == 0x26)
    mode = DSKWRITEINT26;
  else
    mode = DSKREADINT25;

  drv = r->ax & 0x7f; /* according to RBIL, some programs may try with */
                      /* high bit of AL set, so mask it together with AH */
                      /* otherwise we might access a non-existing unit */

  dpbp = get_dpb(drv);

  if (drv >= lastdrive || dpbp == NULL)
  {
    r->ax = 0x201;
    SET_CARRY_FLAG();
    return;
  }

  nblks = r->cx;
  blkno = r->dx;

#ifdef WITHFAT32
  {
    /* if reading / writing block other than bootsecter,
       prevent using INT25,26 on initialized FAT32 */
    if ((blkno != 0) && ISFAT32(dpbp) && (dpbp->dpb_xfatsize != 0))
    {
      r->ax = 0x207;
      SET_CARRY_FLAG();
      return;
    }
  }
#endif

  buf = MK_FP(r->ds, r->bx);

  if (nblks == 0xFFFF)
  {
    /*struct HugeSectorBlock FAR *lb = MK_FP(r->ds, r->bx); */
    blkno = ((struct HugeSectorBlock FAR *)buf)->blkno;
    nblks = ((struct HugeSectorBlock FAR *)buf)->nblks;
    buf = ((struct HugeSectorBlock FAR *)buf)->buf;
  }

  InDOS++;

  if (mode == DSKWRITEINT26)
    DeleteBlockInBufferCache(blkno, blkno, drv, XFR_WRITE);
  r->ax = dskxfer(drv, blkno, buf, nblks, mode);

  CLEAR_CARRY_FLAG();
  if (r->ax != 0)
  {
    SET_CARRY_FLAG();
    if (mode == DSKWRITEINT26)
      setinvld(drv);
  }
  --InDOS;
}

/*
VOID int25_handler(struct int25regs FAR * r) { int2526_handler(DSKREAD,r); }
VOID int26_handler(struct int25regs FAR * r) { int2526_handler(DSKWRITE,r); }
*/

#ifdef TSC
STATIC VOID StartTrace(VOID)
{
  if (bTraceNext)
  {
#ifdef DEBUG
    bDumpRegs = TRUE;
#endif
    bTraceNext = FALSE;
  }
#ifdef DEBUG
  else
    bDumpRegs = FALSE;
#endif
}
#endif

/* this function is called from an assembler wrapper function
   and serves the internal dos calls - int2f/12xx and int2f/4a01,4a02.
*/
struct int2f12regs {
#ifdef I386
#ifdef __WATCOMC__
  /* UWORD gs, fs;  ** GS/FS are protected through SI/DI */
#else
  UWORD high_edx,
#ifdef _MSC_VER
        high_ecx,
#else /* __BORLANDC__ */
        high_ebx,
#endif
        high_eax;
#endif
#endif
  UWORD es, ds;
  UWORD di, si, bp;
  xreg b, d, c, a;
  UWORD ip, cs, flags;
  UWORD callerARG1;             /* used if called from INT2F/12 */
};

extern intvec FAR ASM BIOSInt13;
extern intvec FAR ASM UserInt13;
extern intvec FAR ASM BIOSInt19;


/* WARNING: modifications in `r' are used outside of int2F_12_handler()
 * On input r.AX==0x12xx, 0x4A01 or 0x4A02
 * also handle Windows' DOS notification hooks, r.AH==0x16 and r.AH==0x13
 * along with DRIVER.SYS/DRIVPARAM r.AH=0x08 calls
 */
VOID ASMCFUNC int2F_12_handler(struct int2f12regs FAR *pr)
{
  COUNT rc;
  long lrc;
  UWORD requestedsize;

#define r (*pr)

  r.FLAGS &= ~FLG_CARRY;  /* assume success, ensure carry clear */

  if (r.AH == 0x4a)
  {
    size_t size = 0, offs = 0xffff, realoffs;	/* defaults for no alloc */

    r.ES = offs;
    if (FP_SEG(firstAvailableBuf) == offs) /* HMA present? */
    {
      realoffs = FP_OFF(firstAvailableBuf);
      if (realoffs <= 0xFFF0) {		/* if any free (not yet exthausted) */
        offs = realoffs;
        offs += 15;
        offs &= ~ 15;			/* annoying: buffers may be unaligned */
        size = - offs;				/* bx + di = 10000h */
        if (r.AL == 0x02)			/* allocate HMA space */
        {
          requestedsize = (r.BX + 15) & ~ 15;	/* align to paragraph */
          if (requestedsize < r.BX ||
              requestedsize > size) { /* overflow or OOM */
            size = 0;
            offs = 0xffff;			/* requested more than we have */
          } else {
            UWORD alignment = offs - realoffs;
            size = (UWORD)requestedsize;	/* return rounded size */
            AllocateHMASpace(realoffs, offs + size - 1); /* ! realoffs */
            if ( ((UDWORD)offs + (UDWORD)size) == 0x10000UL ) {
              firstAvailableBuf = MK_FP(0xFFFF, 0xFFFF); /* exhausted */
            } else {
              firstAvailableBuf += size + alignment; /* advance free pointer */
            }
          }
        }
      }
    }
    r.DI = offs;
    r.BX = size;
    return;
  }
  else if (r.AH == 0x13) /* set disk interrupt (13h) handler */
  {
    /* set new values for int13h calls, and must return old values */
    register intvec tmp = UserInt13;
    UserInt13 = MK_FP(r.ds, r.DX);            /* int13h handler to use */
    r.ds = FP_SEG(tmp);  r.DX = FP_OFF(tmp);
    tmp = BIOSInt13;
    BIOSInt13 = MK_FP(r.es, r.BX);            /* int13h handler to restore on reboot */
    r.es = FP_SEG(tmp);  r.BX = FP_OFF(tmp);
    return;
  }
  else if (r.AH == 0x16) /* Window/Multitasking hooks */
  {
#ifdef WIN31SUPPORT  /* See "DOS Internals" or RBIL under DOSMGR for details */
    switch (r.AL)
    {
      /* default: unhandled requests pass through unchanged */
      case 0x0:          /* is Windows active */
      {
        if (winInstanced)
        {
          r.AL = (winStartupInfo.winver >> 8) & 0xFF;  /* winmajor */
          r.AH = winStartupInfo.winver & 0xFF;         /* winminor */
        }
        /* return AX unchanged if Windows not active */
        break;
      } /* 0x0 */
      case 0x0A:         /* identify Windows version */
      {
#if 0 /* windows should answer this? if active, otherwise return not active */
        if (winInstanced)
        {
          r.AX = 0          /* call supported */
          r.BX = winStartupInfo.winver; /* BH=winmajor, BL=winminor */
          r.CX = 2;         /* mode (0002h = standard, 0003h = enhanced) */
        }
#endif        
        /* return AX unchanged if Windows not active */
        break;
      } /* 0x0A */
      case 0x03:          /* Windows Get Instance Data */
      {
        /* This should only be called if AX=1607h/BX=15h is not supported. */
        /* The data returned here corresponds directly with text entries that
           can also be in INSTANCE.386 [which in theory means Windows could
           be updated to support FD kernel without responding to these?].
         */
        DebugPrintf(("get instance data\n"));
        break;
      } /* 0x03 */
      case 0x05:          /* Windows Startup Broadcast */
      {
        /* After receiving this call we activate compatibility changes
           as DOS 5 does, though can wait until 0x07 subfunc 0x01
         */
        /* on entry:
           DX flags, bit 0 is set(=1) for standard mode
           DI Windows version#, major# in high byte
           CX 0, set on exit to nonzero to fail load request
           DS:SI is 0000:0000, for enhanced mode, at most 1 program can
                 set to memory manager calling point to disable V86
           ES:BX is 0000:0000, set to startup structure
          */
        /* r.CX = 0x0; ** redundant and could be set nonzero by another hooked program */
        r.es = FP_SEG(&winStartupInfo);
        r.BX = FP_OFF(&winStartupInfo);
        winStartupInfo.winver = r.di;  /* match what caller says it is */
#if defined __GNUC__
        winseg1 = FP_SEG(&winStartupInfo);
        winseg2 = FP_SEG(&DATASTART);
        winseg3 = FP_OFF(&markEndInstanceData);
#endif
        winInstanced = 1; /* internal flag marking Windows is active */
        DebugPrintf(("Win startup\n"));
        break;
      } /* 0x05 */
      case 0x06:          /* Windows Exit Broadcast */
      {
        /* can do nothing or can remove any changes made
           specifically for Windows, must preserve DS.
           Note: If Windows fatally exits then may not be called.
         */
        winInstanced = 0; /* internal flag marking Windows is NOT active */
        DebugPrintf(("Win exit\n"));
        break;
      } /* 0x06 */
      case 0x07:          /* DOSMGR Virtual Device API */
      {
        DebugPrintf(("Vxd:DOSMGR:%x:%x:%x:%x\n",r.AX,r.BX,r.CX,r.DX));
        if (r.BX == 0x15) /* VxD id of "DOSMGR" */
        {
          switch (r.CX)
          {
            /* default: unhandled requests pass through unchanged */
            case 0x00:    /* query if supported */
            {
              r.CX = winInstanced; /* should always be nonzero if Win is active */
              r.DX = FP_SEG(&nul_dev); /* data segment / segment of DOS drivers */
              r.es = FP_SEG(&winPatchTable); /* es:bx points to table of offsets */
              r.BX = FP_OFF(&winPatchTable);
              break;
            }
            case 0x01:    /* enable Win support, ie patch DOS */
            {
              /* DOS 5+ return with bitflags unchanged, Windows critical section
                 needs are handled without need to patch.  If this
                 function does not return successfully windows will
                 attempt to do the patching itself (very bad idea).
                 On entry BX is bitflags describing support requested,
                 and on return DX is set to which we can support.
                 Note: any we report as unhandled Windows will attempt
                 to patch kernel to handle, probably not a good idea.
                   0001h: enable critical section signals (int 2Ah functions
                          80h/81h) to allow re-entering DOS while InDOS.
                   0002h: allow nonzero local machine ID, ie different VMs
                          report different values.
                          FIXME: does this mean we need to set this or does Windows?
                   0004h: split up binary reads to increase int 2Ah function 84h scheduling
                          / turn Int 21h function 3Fh on STDIN into polling loop
                   0008h: notify Windows of halting due to internal stack errors
                   0010h: notify Windows of logical drive map change ("Insert disk X:")
               */
              r.BX = r.DX;    /* sure we support everything asked for, ;-) */
              r.DX = 0xA2AB;  /* on succes DX:AX set to A2AB:B97Ch */
              r.AX = 0xB97C;
              /* FIXME: do we need to do anything special for FD kernel? */
              break;
            }
            case 0x02:    /* disable Win support, ie remove patches */
            {
              /* Note: if we do anything special in 'patch DOS', undo it here.
                 This is only called when Windows exits, can be ignored.
               */
              r.CX = 0;   /* for compatibility with MS-DOS 5/6 */
              break;
            }
            case 0x03:    /* get internal structure sizes */
            {
              if (r.CX & 0x01) /* size of Current Directory Structure in bytes */
              {
                r.DX = 0xA2AB;   /* on succes DS:AX set to A2AB:B97Ch */
                r.AX = 0xB97C;
                r.CX = sizeof(struct cds);
              }
              else
                r.CX = 0;      /* unknown or unsupported structure requested */
              break;
            }
            case 0x04:    /* Get Instancing Exemptions */
            {
              /* On exit BX is bit flags denoting data that is instanced
                 so Windows need not instance it.  DOS 5&6 fail with DX=CX=0.
                   0001h: Current Directory Structure
                   0002h: System File Table and device status of STDOUT
                   0004h: device driver chain
                   0008h: Swappable Data Area
               */
              r.DX = 0xA2AB;   /* on succes DX:AX set to A2AB:B97Ch */
              r.AX = 0xB97C;
              r.BX = 0; /* a zero here tells Windows to instance everything */
              break;
            }
            case 0x05:    /* get device driver size */
            {
              /* On entry ES:DI points to possible device driver
                 if is not one return with AX=BX=CX=DX=0
                 else return BX:CX size in bytes allocated to driver
                             and DX:AX set to A2AB:B97Ch */
              mcb FAR *smcb = MK_PTR(mcb, (r.ES-1), 0); /* para before is possibly submcb segment */
              /* drivers always start a seg:0 (DI==0), so if not then either
                 not device driver or duplicate (ie device driver file loaded
                 is of multi-driver variety; multiple device drivers in same file,
                 whose memory was allocated as a single chunk)
                 Drivers don't really have a MCB, instead the DOS MCB is broken
                 up into submcbs, which will have a type of 'D' (or 'E')
                 So we check that this is primary segment, a device driver, and owner.
              */
              if (!r.DI && (smcb->m_type == 'D') && (smcb->m_psp == r.ES))
              {
                ULONG size = smcb->m_size * 16ul;
                r.BX = hiword(size);
                r.CX = loword(size);
                r.DX = 0xA2AB;   /* on succes DX:AX set to A2AB:B97Ch */
                r.AX = 0xB97C;
                break;
              }
              r.DX = 0;   /* we aren't one so return unsupported */
              r.AX = 0;
              r.BX = 0;
              r.CX = 0;
              break;
            }
          }
        }
        DebugPrintf(("Vxd:DOSMGR:%x:%x:%x:%x\n",r.AX,r.BX,r.CX,r.DX));
        break;
      } /* 0x07 */
      case 0x08:          /* Windows Init Complete Broadcast */
      {
        DebugPrintf(("Init complete\n"));
        break;
      } /* 0x08 */
      case 0x09:          /* Windows Begin Exit Broadcast */
      {
        DebugPrintf(("Exit initiated\n"));
        break;
      } /* 0x09 */
      case 0x0B:          /* Win TSR Identify */
      {
        DebugPrintf(("TSR identify request.\n"));
        break;
      } /* 0x0B */
      case 0x80:          /* Win Release Time-slice */
      {
        /* This function is generally only called in idle loops */
        DosIdle_hlt();
        r.AX = 0;
        /* DebugPrintf(("Release Time Slice\n")); */
        break;
      } /* 0x80 */
      case 0x81:          /* Win3 Begin Critical Section */
      {
        DebugPrintf(("Begin CritSect\n"));
        break;
      } /* 0x81 */
      case 0x82:          /* Win3 End Critical Section */
      {
        DebugPrintf(("End CritSect\n"));
        break;
      } /* 0x82 */
      case 0x8F:          /* Win4 Close Awareness */
      {
        if (r.DH != 0x01) /* query close */
          r.AX = 0x0;
        /* else r.AX = 0x168F;  don't close -- continue execution */
        break;
      } /* 0x8F */
      default:
        DebugPrintf(("Win call (int 2Fh/16h): %04x %04x %04x %04x\n", r.AX, r.BX, r.CX, r.DX));
        break;
    }
#endif
    return;
  }
  else if (r.AH == 0x46) /* MS Windows WinOLDAP switching */
  {
#ifdef WIN31SUPPORT  /* See "DOS Internals" under DOSMGR or RBIL for details */
    if (r.AL == 0x01)      /* save MCB */
    {
      /* To prevent corruption when dos=umb where Windows 3.0 standard
         writes a sentinel at 9FFEh, DOS 5 will save the MCB marking
         end of conventional memory (ie MCB following caller's PSP memory
         block [which I assume occupies all of conventional memory] into
         the DOS data segment.
         Note: presumably Win3.1 uses the WinPatchTable.OffLastMCBSeg
         when DOS ver > 5 to do this itself.
       */
      /* FIXME: Implement this! */
    }
    else if (r.AL == 0x02) /* restore MCB */
    {
      /* Copy the MCB we previously saved back. */
      /* FIXME: Implement this! */
    }
#endif
    return;
  }
  else if (r.AH == 0x08) /* DRIVER.SYS / DRIVPARAM */
  {
    switch (r.AL)
    {
    case 0x00:                 /* installation check */
      r.AL = 0xff;             /* installed, 0=not installed */
      break;

    case 0x01:                 /* add new block device */
	  /* TODO, see push_ddt() */
      break;

    case 0x02:                 /* execute device driver request */
	  /* TODO */
      break;

    /* Note: see in2f.asm where this is handled */
    case 0x03:                 /* get drive data table */
      r.DS = FP_SEG(&nul_dev);
	  r.DI = FP_OFF(getddt(0));
      break;
    }

    return;
  }
  /* else (r.AH == 0x12) DOS internal functions */

  switch (r.AL)
  {
    case 0x00:
      /* Installation check for INT 2Fh/AH=12h internal DOS services.
       * Input:  AX=1200h.
       * Output: AL=FFh (internal INT 2Fh/AH=12h multiplex handler installed/present).
       */
      r.AL = 0xff;
      break;

#if 0
    case 0x01:
      /* Close current file (internal SFT-level close).
       * Input:  ES:DI -> SFT entry to close.
       * Output: CF=0 on success; CF=1, AX=error code on failure.
       *
       * Decrement SFT.sft_count at ES:DI. If it reaches zero, flush
       * dirty buffers for the file, update the directory entry
       * (time/date/size) if the file is dirty, then mark the SFT free
       * (sft_count = 0xFFFF). For character devices, issue a C_CLOSE
       * driver request if refcount hit zero. Caller has already resolved
       * handle -> SFT; this is the kernel-internal tail of INT 21h/3Eh.
       * NOTE: do NOT decrement InDOS here; caller manages that.
       */
      goto not_implemented;
#endif

    case 0x02:
      /* Get interrupt vector (internal, avoids INT 21h/35h).
       * Input:  CL = interrupt number.
       * Output: ES:BX = current handler address (far ptr from IVT at 0000:CL*4).
       *         CF=0.
       * No state is modified. Equivalent to INT 21h/AH=35h but callable
       * from within the kernel without re-entering INT 21h.
       *
       * Note: not used by FreeDOS kernel
       */
      {
        UBYTE FAR * p = (UBYTE FAR *)getvec(r.CL);
        r.ES = FP_SEG(p);
        r.BX = FP_OFF(p);
        break;
      }

    case 0x03:
      /* Get DOS data segment (internal kernel data segment pointer).
       * Input:  none.
       * Output: DS = segment of DOS DGROUP (kernel data segment).
       *
       * Returns the segment of the DOS kernel data area by loading DS
       * with the segment of the NUL device header, which resides at the
       * start of the DOS data segment. Used internally so code running
       * outside DGROUP can locate kernel variables and tables.
       */
      r.DS = FP_SEG(&nul_dev);
      break;

    case 0x04:
      /* Normalize path separator / DBCS lead-byte check.
       * Input:  character to test passed on stack
       * Output: If AL is '/' (2Fh), '\' (5Ch), or the current SWITCHAR:
       *           AL = '\' (5Ch), ZF=1.
       *         If AL is a DBCS lead byte (per DBCS table):
       *           AL unchanged, ZF=0, DL=1 (flag: skip next byte).
       *         Otherwise:
       *           AL unchanged, ZF=0, DL=0.
       *
       * Allows the DOS path parser to canonicalize separators while
       * guarding against misinterpreting the trail byte of a DBCS
       * two-byte character as a path separator. Check DBCS lead-byte
       * ranges from the DBCS table (see INT 21h/AH=63h / DBCS_TAB).
       *
       * Note: not used by FreeDOS kernel
       */
      {
        unsigned char c = r.callerARG1 & 0xFF;

        /* --- separator check first (takes priority over DBCS) --- */
        if (c == '/' || c == '\\' || c == (unsigned char)switchar)
        {
          r.AL = '\\';                  /* canonicalize to backslash  */
          r.FLAGS |= FLG_ZERO;          /* ZF=1                       */
          r.DL = 0;
        } else 
        {
          /* --- DBCS lead-byte check --- */
          BOOL nlsIsDBCS(UBYTE ch); /* only used here and in nls.c */
          r.DL = (nlsIsDBCS(c))?1:0; 
          /* AL unchanged */
          r.FLAGS &= ~FLG_ZERO;           /* ZF=0                       */
        }
        
        break;
      }
      
    case 0x05:
      /* Output character to standard output (kernel-internal).
       * Input:  AL = character to write.
       * Output: none (AL preserved by convention).
       *
       * Write AL to STDOUT (JFT[1] of current PSP) via the DOS
       * character-output path, with CTRL-C/CTRL-BREAK checking.
       * Equivalent to INT 21h/AH=02h but does NOT increment InDOS,
       * making it safe to call from within the kernel. May trap to
       * INT 23h on break. Modifies: flags; AL preserved.
       *
       * Note: not used by FreeDOS kernel
       */
      {
        check_handle_break(&syscon);
        write_char_stdout(r.AL);
        break;
      }
      
    case 0x06:
      /* Invoke critical error handler (INT 24h) with explicit parameters.
       * Input:  callerARG1 high byte = error type/flags (EFLG_CHAR etc.);
       *         callerARG1 low byte  = drive number (if not char device);
       *         DI = DOS error code;
       *         BP:SI = far pointer to device header.
       * Output: AL = user response (0=ignore, 1=retry, 2=abort, 3=fail).
       *
       * Directly invokes CriticalError() with the supplied drive number,
       * error code, and device header pointer. The EFLG_CHAR bit in the
       * high byte of callerARG1 selects drive 0 (character device) vs.
       * the low-byte drive number (block device). Differs from 0x0A in
       * that all parameters are supplied explicitly by the caller rather
       * than being derived from the current drive and stack.
       * Modifies: AL.
       */

      /* code, drive number, error, device header */
      r.AL = CriticalError(r.callerARG1 >> 8,
                           (r.callerARG1 & (EFLG_CHAR << 8)) ? 0 :
                           r.callerARG1 & 0xff, r.DI, MK_FP(r.BP, r.SI));
      break;

#if 0
    case 0x07:
      /* Make disk buffer most-recently used (move to MRU head of LRU chain).
       * Input:  DS:BX -> buffer header in the DOS buffer cache chain.
       * Output: CF=0. No registers modified except flags.
       *
       * Unlink the buffer header at DS:BX from its current position in
       * the doubly-linked LRU chain and relink it at the MRU (head) end.
       * The chain anchor is at DOS_DATA buf_head. Buffer header layout
       * (DOS 4.x): +00h next ptr (WORD), +02h prev ptr, +04h drive
       * (FFh=free), +05h flags (dirty=40h, FAT=10h, dir=08h),
       * +06h sector number (DWORD), +0Ah DPB ptr.
       * NOTE: 0x0F performs the same operation but with pointer in DS:DI.
       */
      goto not_implemented;
#endif

    case 0x08:
      /* Decrease SFT reference count (without closing).
       * Input:  ES:DI -> SFT entry.
       * Output: AX = original (pre-decrement) sft_count value.
       *
       * Reads the current sft_count from the SFT entry at ES:DI into AX,
       * then decrements sft_count. If the decremented value reaches zero
       * it is decremented once more (wrapping to 0xFFFF) to mark the SFT
       * as free, matching the DOS convention that 0xFFFF means unused.
       * Unlike 0x01 this does NOT flush buffers, update the directory
       * entry, or issue a device-driver close; it is a raw refcount
       * adjustment used during handle duplication and inheritance cleanup.
       * Modifies: AX, ES:DI->sft_count.
       */
      {
        sft FAR *p = MK_FP(r.ES, r.DI);

        r.AX = p->sft_count;

        if (--p->sft_count == 0)
          --p->sft_count;
      
        break;
      }

#if 0
    case 0x09:
      /* Flush and free disk buffer (write-back and release).
       * Input:  DS:BX -> buffer header.
       * Output: CF=0 on success; CF=1, AX=DOS error code on write failure.
       *
       * If the buffer's dirty flag (bit 40h of flags byte) is set, write
       * the buffer's sector back to disk via the driver for the drive in
       * header+04h. If this is a FAT buffer (flag bit 10h) and the DPB
       * indicates multiple FAT copies, also write mirror FAT sectors
       * (one write per additional FAT, offset by sectors-per-FAT each).
       * After writing, clear the dirty flag. Then mark the buffer free
       * by setting the drive byte (header+04h) to FFh. Do NOT relink in
       * the LRU chain here; caller uses 0x07/0x0F for that.
       */
      goto not_implemented;
#endif

    case 0x0a:
      /* Perform critical error using current drive context (INT 24h).
       * Input:  callerARG1 = DOS error code (passed on stack by caller);
       *         default_drive = current default drive number.
       * Output: AL = user response (0=ignore, 1=retry, 2=abort, 3=fail).
       *         CF=1 normally; CF=0 if AL=1 (retry requested).
       *
       * Invokes CriticalError() using the current default drive and its
       * associated DPB device header, with the error code taken from the
       * caller's stack argument. Differs from 0x06 in that drive number
       * and device header are derived from the current drive context
       * rather than being passed explicitly. Sets CF on return; clears
       * CF only if the user chose retry (AL=1).
       */
      /* code, drive number, error, device header */
      r.AL = CriticalError(0x38, /* ignore/retry/fail - based on RBIL possible return values */
                           default_drive,
                           r.callerARG1, /* error, from RBIL passed on stack */
                           CDSp[(WORD)default_drive].cdsDpb->dpb_device);
      r.FLAGS |= FLG_CARRY;
      if (r.AL == 1) r.FLAGS &= ~FLG_CARRY;  /* carry clear if should retry */
      break;

    case 0x0b:
      /* Handle sharing violation (conditionally invoke INT 24h).
       * Input:  ES:DI -> SFT entry for the previously-opened conflicting file;
       *         callerARG1 = DOS error code (on stack).
       * Output: AX = 0x0020 (sharing violation error code);
       *         CF=1 normally; CF=0 if retry was chosen (AL=1 from INT 24h).
       *
       * Examines the conflicting SFT's open mode. If the file was opened
       * via FCB (O_FCB set) or in compatibility mode without NOINHERIT,
       * invokes CriticalError() (INT 24h) to give the user a chance to
       * retry. Otherwise returns immediately with CF set and AX=0x0020.
       * CF is cleared only if CriticalError returns AL=1 (retry).
       * Always sets AX=0x0020 (DE_SHARE) on return regardless of path taken.
       */
      {
        /* ES:DI = SFT for previous open of file */
        sft FAR *sftp = MK_FP(r.ES, r.DI);
        /* default to don't retry, ie fail/abort/etc other than retry */
        r.FLAGS |= FLG_CARRY;
        /* from RBIL if SFT for FCB or compatibility mode without NOINHERIT call int24h */
        if ((sftp->sft_mode & O_FCB) || !(sftp->sft_mode & (O_SHAREMASK | O_NOINHERIT)))
        {
          r.AL = CriticalError(0x38, /* ignore/retry/fail - ??? */
                               default_drive,
                               r.callerARG1, /* error, from RBIL passed on stack */
                               CDSp[(WORD)default_drive].cdsDpb->dpb_device);
          /* clear carry if should retry */
          if (r.AL == 1) r.FLAGS &= ~FLG_CARRY;
        }
        r.AX = 0x20;  /* error sharing violation */
      }
      break;

    case 0x0c:
      /* Perform device-open and set SFT owner PSP (open notification).
       * Input:  lpCurSft -> current SFT entry (global, set before call).
       * Output: none; CF not modified.
       *
       * If the SFT entry refers to a character device (SFT_FDEVICE set),
       * issues a C_OPEN request to the device driver via execrh() so the
       * driver can perform any per-open initialization. Regardless of
       * device type, records the current PSP segment (cu_psp) in the
       * SFT's sft_psp field to establish ownership. Called during file
       * open processing after the SFT has been allocated and filled in.
       * Modifies: lpCurSft->sft_psp; device driver internal state if char device.
       */

      if (lpCurSft->sft_flags & SFT_FDEVICE)
      {
        request rq;

        rq.r_unit = 0;
        rq.r_status = 0;
        rq.r_command = C_OPEN;
        rq.r_length = sizeof(request);
        execrh((request FAR *) & rq, lpCurSft->sft_dev);
      }

      /* just do it always, not just for FCBs */
      lpCurSft->sft_psp = cu_psp;
      break;

    case 0x0d:
      /* Get current DOS date and time (packed internal format).
       * Input:  none.
       * Output: AX = packed date word (as returned by dos_getdate());
       *         DX = packed time word (as returned by dos_gettime()).
       *
       * Retrieves the current system date and time in the DOS internal
       * packed format. AX receives the date (year/month/day encoded) and
       * DX receives the time (hours/minutes/seconds encoded). This is the
       * kernel-internal equivalent of INT 21h/AH=2Ah+2Ch combined into a
       * single call, used by file-stamping code inside the kernel.
       */

      r.AX = dos_getdate();
      r.DX = dos_gettime();
      break;

#if 0
    case 0x0e:
      /* Mark all disk buffers unreferenced (clear LRU referenced bits).
       * Input:  none.
       * Output: CF=0. No registers modified except flags (internally
       *         uses AX/BX/ES but must restore them).
       *
       * Walk the entire buffer cache chain from head to tail. On each
       * buffer header, clear the "referenced" / "recently-touched" bit
       * (implementation-defined bit in the flags byte used by the LRU
       * refill pass). This prepares for a subsequent 0x10 scan to find
       * a victim buffer. Called before the buffer-allocation sweep.
       */
      goto not_implemented;

    case 0x0f:
      /* Make buffer most recently used (pointer in DS:DI).
       * Input:  DS:DI -> buffer header.
       * Output: CF=0. No registers modified except flags.
       *
       * Identical in effect to 0x07 (relink header to MRU head of the
       * doubly-linked LRU chain), but the buffer pointer is passed in
       * DI rather than BX. Both subfunctions coexist because different
       * internal callers use different register conventions.
       * See 0x07 for buffer header layout details.
       */
      goto not_implemented;

    case 0x10:
      /* Find unreferenced disk buffer (LRU victim selection).
       * Input:  none (chain walked from LRU tail toward MRU head).
       * Output: DS:DI -> chosen buffer header; ZF=0 if found, ZF=1 if none.
       *
       * Scan the buffer chain from the LRU (tail) end forward. Return
       * the first header whose drive byte == FFh (free) OR whose
       * "referenced" bit is clear (see 0x0E). If no such buffer exists
       * return with ZF=1 so the caller can flush one explicitly.
       * Modifies: DI, flags; DS may be set to DOS DGROUP on return.
       * NOTE: call 0x0E first to clear referenced bits before calling
       * this function, otherwise all in-use buffers may appear referenced.
       */
      goto not_implemented;
#endif

    case 0x11:
      /* Normalize ASCIIZ filename (uppercase and convert path separators).
       * Input:  DS:SI -> source ASCIIZ filename string.
       *         ES:DI -> destination buffer for normalized string.
       * Output: ES:DI buffer filled with normalized, NUL-terminated string.
       *
       * Copies the source string to the destination, converting lowercase
       * ASCII letters (a-z) to uppercase (A-Z) and forward slashes ('/')
       * to backslashes ('\'). Processes until and including the NUL
       * terminator. 
       * NOTE: DBCS characters are not specially handled in this implementation.
       */
    {
      char c;
      char FAR *s = MK_FP(r.DS, r.SI);
      char FAR *t = MK_FP(r.ES, r.DI);

      do
      {
        c = *s++;
        /* uppercase character */
        /* for now, ASCII only because nls.c cannot handle DS!=SS */
        if (c >= 'a' && c <= 'z')
          c -= 'a' - 'A';
        else if (c == '/')
          c = '\\';
        *t++ = c;
      }
      while (c);
      break;
    }

    case 0x12:
      /* Get length of ASCIIZ string at ES:DI.
       * Input:  ES:DI -> NUL-terminated string.
       * Output: CX = length of string including the NUL terminator byte
       *
       * See also 0x25 which performs the same operation on DS:SI.
       */
      r.CX = fstrlen(MK_FP(r.ES, r.DI)) + 1;
      break;
      
    case 0x13:
      /* Uppercase a single character (ASCII, kernel-internal).
       * Input:  character in callerARG1 low byte (pushed on stack by caller).
       * Output: AL = uppercased character.
       *
       * Reads the character from callerARG1 (the value pushed on the
       * caller's stack before the INT 2Fh), converts lowercase ASCII
       * letters (a-z) to uppercase (A-Z), and returns the result in AL.
       * NOTE: for now, ASCII only because nls.c cannot handle DS!=SS
       */
      r.AL = (unsigned char)r.callerARG1;
      if (r.AL >= 'a' && r.AL <= 'z')
        r.AL -= 'a' - 'A';
      break;

    case 0x14:
      /* Compare two far pointers for exact equality (does not normalize addresses).
       * Input:  DS:SI = first far pointer; ES:DI = second far pointer.
       * Output: ZF=1 if DS==ES and SI==DI (equal); ZF=0 otherwise.
       *         CF=0. No other registers modified.
       *
       * Note: not used by FreeDOS kernel; may be used by MSDOS
       *       used by Lantastic
       */
      {
        if ((r.DS == r.ES) && (r.SI == r.DI)) {
          r.FLAGS |= FLG_ZERO;
        } else {
          r.FLAGS &= ~FLG_ZERO;
        }
      
        break;
      }

#if 0
    case 0x15:
      /* Flush (write-back) a single dirty disk buffer.
       * Input:  DS:DI -> buffer header (some refs ES:DI; use DS:DI per RBIL).
       * Output: CF=0 on success; CF=1, AX=DOS error code on write failure.
       *
       * If the buffer's dirty flag (bit 40h of flags byte) is set, write
       * the sector back via the driver for the drive in header+04h. If
       * this is a FAT buffer (flag bit 10h) and the DPB indicates multiple
       * FAT copies, also write each mirror FAT sector (offset by
       * sectors-per-FAT per copy). Clear the dirty flag after a successful
       * write. Do NOT mark the buffer free (drive byte unchanged); that is
       * done by 0x09. Do NOT relink in the LRU chain.
       */
      goto not_implemented;
#endif

    case 0x16:
      /* Get address of SFT entry by system file number (used by NET.EXE).
       * Input:  BX = system file table entry number (SFN, as returned
       *              by INT 2Fh/AX=1220h).
       * Output: On success: ES:DI -> SFT entry; BX = relative entry index
       *                     within its SFT block; CF=0.
       *         On failure: CF=1 (invalid SFN).
       *
       * Walks the SFT chain via idx_to_sft_() to locate the SFT entry
       * corresponding to the given system file number. Returns both the
       * far pointer to the entry (in ES:DI, also stored in lpCurSft) and
       * the relative index within the SFT block (in BX). Used by network
       * redirectors (NET.EXE) to obtain direct access to an SFT entry
       * given a previously-obtained SFN. See also 0x30 (find by SFN,
       * Windows 95 variant).
       */
      {
        int rel_idx = idx_to_sft_(r.BX);

        if (rel_idx == -1)
          goto  error_carry;

        r.BX = rel_idx;
        r.ES = FP_SEG(lpCurSft);
        r.DI = FP_OFF(lpCurSft);
        break;
      }

    case 0x17:
      /* Get Current Directory Structure (CDS) for a drive (used by NET.EXE).
       * Input:  callerARG1 low byte = drive number (0=A:, 1=B:, ...) pushed
       *         on stack by caller (e.g. "push 2" for C:).
       * Output: On success: DS:SI -> CDS entry for the drive; CF=0.
       *         On failure: CF=1 (invalid or out-of-range drive number).
       *
       * Used by network redirectors to obtain the CDS for a drive without
       * going through the full validation path. A secondary use is
       * to determine sizeof(CDS entry) from the returned pointer.
       *
                    STACK: drive (0=A:,1=B,...)
                    ; returns
                    ;   CF set if error
                    ;   DS:SI pointer to CDS for drive
                    ;
                    ; called like
                    ;   push 2 (c-drive)
                    ;   mov ax,1217
                    ;   int 2f
       */
      {
        struct cds FAR *cdsp = get_cds_unvalidated(r.callerARG1 & 0xff);

        if (cdsp == NULL)
          goto error_carry;

        r.DS = FP_SEG(cdsp);
        r.SI = FP_OFF(cdsp);
        break;
      }

    case 0x18:
      /* Get pointer to caller's saved register frame (user_r).
       * Input:  none.
       * Output: DS:SI -> user_r (the iregs structure saved on INT 21h entry
       *         containing the calling program's register state).
       *
       * Returns a far pointer to the global user_r register save area in
       * DS:SI. This allows internal DOS code and network redirectors to
       * inspect or modify the register values that will be restored to the
       * caller when the current INT 21h call returns. Used by SHARE and
       * network components that need to patch return values after the fact.
       */

      r.DS = FP_SEG(user_r);
      r.SI = FP_OFF(user_r);
      break;

    case 0x19:
      /* Set default drive (internal, validates against LASTDRIVE).
       * Input:  AL = drive number (0=A:, 1=B:, ...).
       * Output: On success: AL = LASTDRIVE count (number of valid drives),
       *                     CF=0.
       *         On invalid drive: CF=1, AX=000Fh (invalid drive).
       *
       * Validate AL against lastdrive. If valid, set the DOS internal
       * current-drive byte to AL and update the internal current-CDS
       * pointer to CDS_array + AL * sizeof(struct cds). Then trigger a
       * media check / DPB build for the new drive. Return the total 
       * number of valid drives in AL.
       *
       * Note: not used by FreeDOS kernel
       */
      {
        UBYTE drv = r.AL;

        if (drv >= lastdrive || get_cds_unvalidated(drv) == NULL)
        {
          r.AX = -DE_INVLDDRV;   /* AX = 0x000F */
          goto error_carry;
        }
        r.AL = DosSelectDrv(drv);

        break;
      }
      
    case 0x1a:
      /* Get drive number from file path (parse leading drive letter).
       * Input:  DS:SI -> ASCIZ path or drive-letter string (e.g. "C:\...").
       * Output: AL = 0    if no explicit drive letter (default drive used).
       *         AL = 1..26 for explicit A:..Z:.
       *         AL = 0FFh  if invalid/unrecognized drive letter.
       *         CF=0 always (errors expressed via AL=FFh).
       *
       * Note: used by Lantastic when trying to map a drive
       */
      {
        char FAR *p = MK_FP(r.DS, r.SI);
        unsigned char c = p[0];

        if (!c || p[1] != ':')         /* "" or path without X: drive letter */
        {
          r.AL = 0;                    /* no drive letter present, use default */
        }
        else
        {
          c |= 0x20;                   /* map to lower case if letter */
          if (c >= 'a' && c <= 'z')
          {
            r.AL = c - 'a' + 1;        /* 'A'->1, 'B'->2, ... 'Z'->26 */
            r.SI += 2;                 /* RBIL says DS:SI returns path without
                                          drive letter, suggests MSDOS just
                                          uses DS:SI incrementing as it parses */
          }
          else
          {
            r.AL = 0xff;               /* out of range, invalid drive letter */
          }
        }
        
        break;
      }
      
    case 0x1b:
      /* Return number of days in February for a given year (leap year check).
       * Input:  CL = year modulo 4 (low 2 bits used; CL & 3 == 0 means leap year).
       * Output: AL = 29 if leap year (CL & 3 == 0); AL = 28 otherwise.
       *
       * Tests the low two bits of CL. If both are zero the year is divisible
       * by 4 and is treated as a leap year (29 days). Otherwise returns 28.
       * NOTE: this simple test is only valid until 2099; the century-year
       * exception (years divisible by 100 but not 400 are not leap years)
       * is not handled, but is irrelevant within the DOS epoch.
       */
      r.AL = (r.CL & 3) ? 28 : 29;
      break;

#if 0 /* unlikely to be used */
    case 0x1c:
      /* Checksum memory block (16-bit running sum with carry fold).
       * Input:  DS:SI -> start of buffer; CX = byte count;
       *         DX = initial checksum seed (usually 0).
       * Output: DX = updated checksum; SI advanced by CX; CX = 0.
       *
       * Used during init to checksum device-driver images.
       * NOTE: differs from 0x1D which uses a plain 8-bit truncated sum.
       */
      {
        UWORD FAR *p = MK_FP(r.DS, r.SI);
        UWORD sum = r.DX;
        UWORD count = r.CX;
        UWORD words = count >> 1;        /* number of full word pairs */

        while (words--)
        {
          UWORD prev = sum;
          sum += *p++;
          if (sum < prev)                /* carry out: fold back in (ones-complement style) */
            sum++;
        }

        if (count & 1)                   /* odd trailing byte: zero-extend to word */
        {
          UWORD prev = sum;
          sum += (UWORD)*((UBYTE FAR *)p);
          if (sum < prev)
            sum++;
        }

        r.DX = sum;
        r.SI += count;                   /* advance SI by original byte count */
        r.CX = 0;

        break;
      }

    case 0x1d:
      /* Sum memory block (8-bit truncated running sum, no carry fold).
       * Input:  DS:SI -> buffer; CX = byte count.
       * Output: AL = 8-bit sum (truncated, carry ignored);
       *         SI advanced by CX; CX = 0.
       *
       * Differs from 0x1C which uses a 16-bit carry-folded sum.
       * Note: not used by FreeDOS kernel
       */
      {
        UBYTE FAR *p = MK_FP(r.DS, r.SI);
        UBYTE sum = r.AL;
        UWORD count = r.CX;

        r.SI += count;                  /* advance SI before count is consumed */
        while (count--)
          sum += *p++;

        r.AL = sum;
        r.CX = 0;

        break;
      }
#endif
      
    case 0x1e:
      /* Compare ASCIIZ filenames for equivalence (case-insensitive, ASCII range only).
       * Input:  DS:SI -> first  ASCIIZ filename
       *         ES:DI -> second ASCIIZ filename
       * Output: ZF=1 if filenames equivalent; ZF=0 if not equivalent.
       *
       * Note: not used by FreeDOS kernel
       */
      {
        const char FAR *s1 = MK_FP(r.DS, r.SI);
        const char FAR *s2 = MK_FP(r.ES, r.DI);
        char c1, c2;

        do {
          c1 = *s1++;
          c2 = *s2++;
          if (c1 >= 'a' && c1 <= 'z') c1 -= 'a' - 'A';
          if (c2 >= 'a' && c2 <= 'z') c2 -= 'a' - 'A';
        } while (c1 && c1 == c2);

        if (c1 == c2)
          r.FLAGS |=  FLG_ZERO;  /* ZF=1: equivalent */
        else
          r.FLAGS &= ~FLG_ZERO;  /* ZF=0: not equivalent */

        break;
      }
      
    case 0x1f:
      /* Build a temporary Current Directory Structure for a drive letter.
       * Input:  callerARG1 low byte = drive letter ASCII ('A'..'Z' or 'a'..'z')
       *         pushed on stack by caller.
       * Output: On success: ES:DI -> TempCDS (filled-in temporary CDS); CF=0.
       *         On failure: CF=1 (invalid letter or drive out of range).
       *
       * Fills TempCDS with a minimal CDS: path set to "X:\", DPB pointer 
       * and flags copied from the real CDS if the drive is active 
       * (CDSPHYSDRV only, no other flags inherited), or NULL/0 if inactive. 
       * Start cluster and parameter fields set to 0xFFFF. 
       */
      {
        /* the drive is passed as ASCII letter ('A'-'z'), not a number (1-26),
         * so masks to lower 5 bits and adjusts from 1 based to 0 based.
         */
        int drv = (r.callerARG1 & 0x1f) - 1;
        struct cds FAR *cdsp;
        #if 0
        if ((unsigned)drv >= nblkdev) /* if drv < 0 or exceeds # of block devices */
            goto error_carry;
        #endif

        cdsp = get_cds_unvalidated(drv);
        if (cdsp == NULL)
            goto error_carry;
        
        memset(&TempCDS, 0, sizeof(TempCDS));
        strcpy(TempCDS.cdsCurrentPath, "?:\\");
        *TempCDS.cdsCurrentPath = (BYTE)(r.callerARG1 & 0xff);
        TempCDS.cdsBackslashOffset = 2;
        if (cdsp->cdsFlags)
        {
          TempCDS.cdsDpb = cdsp->cdsDpb;
          TempCDS.cdsFlags = CDSPHYSDRV;    /* don't inherit CDS flags */
        }
        else
        {
          TempCDS.cdsDpb = NULL;
          TempCDS.cdsFlags = 0;
        }
        TempCDS.cdsStrtClst = 0xffff;
        TempCDS.cdsParam = 0xffff;
        TempCDS.cdsUserData = 0xffff;
        r.CX = sizeof(TempCDS);
        r.ES = FP_SEG(&TempCDS);
        r.DI = FP_OFF(&TempCDS);
        break;
      }

    case 0x20:
      /* Get Job File Table (JFT) entry pointer for a handle.
       * Input:  BX = file handle (index into current PSP's JFT).
       * Output: On success: ES:DI -> JFT byte entry (ps_filetab[BX]);
       *                     CF=0.
       *         On failure: AL = negated DE_INVLDHNDL; CF=1.
       *
       * Validates BX against ps_maxfiles in the current PSP. If valid,
       * returns a far pointer to the JFT byte at ps_filetab[BX] in ES:DI.
       * The JFT byte contains the SFN (system file number) that maps this
       * handle to an SFT entry. Used by network redirectors and internal
       * code that need to directly read or modify the JFT without going
       * through the full handle-resolution path.
       */
      {
        psp FAR *p = MK_FP(cu_psp, 0);
        unsigned char FAR *idx;

        if (r.BX >= p->ps_maxfiles)
        {
          r.AL = -DE_INVLDHNDL;
          goto error_carry;
        }
        idx = &p->ps_filetab[r.BX];
        r.ES = FP_SEG(idx);
        r.DI = FP_OFF(idx);

        break;
      }

    case 0x21:
      /* Canonicalize (truename) a file path (internal wrapper for INT 21h/60h).
       * Input:  DS:SI -> source ASCIIZ path (possibly relative, with '/', etc.).
       *         ES:DI -> destination buffer for canonical ASCIIZ path.
       * Output: ES:DI buffer filled with fully qualified canonical path.
       *         CF and AX set by DosTruename() on error.
       *
       * Calls DosTruename() to resolve the source path to a fully qualified,
       * normalized absolute path in the destination buffer. Handles drive
       * letters, relative paths, '.' and '..' components, and '/' -> '\'
       * conversion. This is the kernel-internal equivalent of INT 21h/AH=60h
       * (TRUENAME), used by internal callers that already have DS:SI and
       * ES:DI set up rather than going through the full INT 21h dispatch.
       */
      rc = DosTruename(MK_FP(r.DS, r.SI), MK_FP(r.ES, r.DI));
      r.AX = rc;
      goto short_check;

    case 0x22:
      /* Set extended error information (inject error state for INT 21h/59h).
       * Input:  DS:SI -> 4-byte (minimum) error block:
       *           +00h WORD  = extended error code (see INT 21h/AH=59h).
       *           +02h BYTE  = error class.
       *           +03h BYTE  = suggested action.
       *           +04h BYTE  = locus (some callers supply 5 bytes).
       * Output: CF=0.
       *
       * Copy the fields from DS:SI into the DOS-internal extended-error
       * state variables (CritErrCode, CritErrClass, CritErrAction,
       * CritErrLocus) that are returned by INT 21h/AH=59h. Used by
       * network redirectors and TSRs to inject a specific error to be
       * reported to the next INT 21h caller. Equivalent in purpose to
       * INT 21h/AH=5Dh/AL=0Ah but reached via the internal 2F/12 path.
       */
      goto not_implemented;

    case 0x23:
      /* Check if a name refers to a character device.
       * Input:  DirEntBuffer.dir_name = 11-byte FCB-format filename to test
       *         (set as a side-effect of a preceding directory search).
       * Output: On success (is a device): BH = device attribute word low byte
       *                                   (dh_attr of the device header); CF=0.
       *         On failure (not a device): CF=1.
       *
       * Calls IsDevice() with the name from DirEntBuffer.dir_name. If a
       * matching device header is found in the device chain, returns its
       * attribute byte in BH and clears CF. If no match, sets CF. Used
       * during open/create processing to distinguish character devices
       * from disk files before allocating an SFT entry.
       */
      {
        struct dhdr FAR *dhp;

        dhp = IsDevice((BYTE FAR *) DirEntBuffer.dir_name);
        if (dhp == NULL) goto error_carry;
        r.BH = dhp->dh_attr;
      }
      break;

    case 0x24:
      /* Set sharing retry delay and retry count (SHARE internals).
       * Input:  CX = delay loop count (outer retry delay iterations).
       *         DX = retry count (number of retries before giving up).
       * Output: CF=0.
       *
       * Store CX into the DOS-internal SharingDelay word and DX into
       * the SharingRetries word in DGROUP. These values are consulted
       * by the sharing-violation retry loop in file open and lock
       * operations (the same values set by INT 21h/AH=44h/CX=440Bh
       * at the public API level). Both words live in the DOS data
       * segment.
       */
      goto not_implemented;

    case 0x25:
      /* Get length of ASCIIZ string at DS:SI.
       * Input:  DS:SI -> NUL-terminated string.
       * Output: CX = length of string including the NUL terminator byte
       *
       * Identical in behavior to 0x12 but operates on DS:SI rather than ES:DI, 
       */
      r.CX = fstrlen(MK_FP(r.DS, r.SI)) + 1;
      break;

    case 0x26:
      /* Open file (internal wrapper for DosOpen, legacy compatibility mode).
       * Input:  DS:DX -> ASCIIZ filename to open.
       *         CL = open mode flags (O_RDONLY / O_WRONLY / O_RDWR etc.).
       * Output: On success: AX = file handle (SFN); CF=0.
       *         On failure: AX = DOS error code; CF=1.
       *
       * Clears CF and CritErrCode, then calls DosOpen() with O_LEGACY|O_OPEN
       * combined with the mode flags from CL. The attribute parameter is 0.
       * Result is checked via long_check: on success AX = handle; on failure
       * AX = error code and CF set. This is the kernel-internal open used by
       * code that needs to open a file without going through INT 21h/3Dh.
       * Modifies: AX, FLAGS, CritErrCode.
       */
      CritErrCode = SUCCESS;
      lrc = DosOpen(MK_FP(r.DS, r.DX), O_LEGACY | O_OPEN | r.CL, 0);
      goto long_check;

    case 0x27:
      /* Close file by handle (internal wrapper for DosClose).
       * Input:  BX = file handle to close.
       * Output: CF=0 on success; CF=1, AX=error code on failure.
       *
       * Clears CF and CritErrCode, then calls DosClose() with the handle
       * in BX. Result is checked via short_check: on success CF remains
       * clear; on failure AX = error code and CF is set. This is the
       * kernel-internal close used by code that needs to close a handle
       * without going through INT 21h/3Eh.
       * Modifies: AX, FLAGS, CritErrCode.
       */
      CritErrCode = SUCCESS;
      rc = DosClose(r.BX);
      goto short_check;

#ifdef WITHFAT32
#ifdef WITHLFNAPI
    case 0x42:
      /* 64-bit move file pointer (LFN/FAT32 extended seek).
       * Input:  DS:DX -> 64-bit file position (UDWORD[2], little-endian);
       *         CL = seek method (0=from start, 1=from current, 2=from end);
       *         BX = file handle.
       * Output: Same as 0x28 on success: DX:AX = new 32-bit file position; CF=0.
       *         On failure: AX = error code; CF=1.
       *
       * Only the lower 32 bits of the file position are supported.
       */
    {
      /* r.(DS:DX) points to 64-bit file position instead of r.(CX:DX) being 32-bit file position */
      UDWORD FAR *filepos = MK_FP(r.DS, r.DX);
      if (*(filepos+1) != 0) /* we currently only handle lower 32 bits, upper 32 bits must be 0 */
        goto error_invalid;
      r.BP = 0x4200 + r.CL;
      r.CX = hiword(*filepos);
      r.DX = loword(*filepos);
      /* fall through to 32-bit move file pointer (0x28) */
    }
#endif
#endif
    case 0x28:
      /* Move file pointer / seek (internal wrapper for DosSeek).
       * Input:  BX = file handle;
       *         CX:DX = 32-bit signed offset (CX=high word, DX=low word);
       *         BP = seek method biased: 0x4200=from start, 0x4201=from current,
       *              0x4202=from end (BP value passed by internal callers).
       * Output: On success: DX:AX = new 32-bit file position; CF=0.
       *         On failure: AX = error code; CF=1.
       *
       * Validates BP is in range 0x4200..0x4202; if not, jumps to error_invalid.
       * Calls DosSeek() with handle BX, offset MK_ULONG(CX,DX), and method
       * BP & 0xFF. On success, splits the returned position into DX (high) and
       * AX (low). NOTE: RBIL documents that MS-DOS sets a dummy stack frame
       * pointer and moves BP to AX before calling; FreeDOS does not replicate
       * that internal mechanism but achieves the same observable result.
       * Modifies: AX, DX, FLAGS, CritErrCode.
       */
      /*
       * RBIL says: "sets user stack frame pointer to dummy buffer,
       * moves BP to AX, performs LSEEK, and restores frame pointer"
       * We obviously don't do it like that. Does this do any harm?! --L.G.
       */
      CritErrCode = SUCCESS;
      if (r.BP < 0x4200 || r.BP > 0x4202)
        goto error_invalid;
      lrc = DosSeek(r.BX, MK_ULONG(r.CX, r.DX), r.BP & 0xff, &rc);
      if (rc == SUCCESS)
      {
        r.DX = (UWORD)(lrc >> 16);
        r.AX = (UWORD) lrc;
      }
      goto short_check;

    case 0x29:
      /* Read from file (internal wrapper for DosRead).
       * Input:  BX = file handle;
       *         CX = number of bytes to read;
       *         DS:DX -> destination buffer.
       * Output: On success: AX = number of bytes actually read; CF=0.
       *         On failure: AX = DOS error code; CF=1.
       *
       * Clears CritErrCode, then calls DosRead() with handle BX,
       * count CX, and buffer DS:DX. Result is checked via long_check:
       * on success AX = bytes read (may be less than CX at EOF); on
       * failure AX = error code and CF set. Kernel-internal equivalent
       * of INT 21h/AH=3Fh without the full INT 21h dispatch overhead.
       * Modifies: AX, FLAGS, CritErrCode.
       */
      CritErrCode = SUCCESS;
      lrc = DosRead(r.BX, r.CX, MK_FP(r.DS, r.DX));
      goto long_check;

    case 0x2a:
      /* Set FASTOPEN entry point (stub; FASTOPEN not implemented).
       * Input:  ES:DI -> FASTOPEN handler entry point (FAR); BX = 0 to install,
       *         nonzero to uninstall. (Per PC-DOS 4 FASTOPEN convention.)
       * Output: CF=0.
       *
       * In a full implementation, stores ES:DI into the DOS-internal
       * FastOpen_Entry FAR pointer (or zeros it if BX != 0), enabling the
       * DOS name-lookup path to call through to a FASTOPEN directory cache
       * before going to disk. FreeDOS currently does nothing here; the
       * function simply clears CF and returns, making FASTOPEN installs
       * silently succeed without effect.
       */

      break;

    case 0x2b:
      /* Device I/O Control (internal wrapper for DosDevIOctl).
       * Input:  BP = IOCTL function code (0x4400..0x44FF, i.e. INT 21h/44xx);
       *         BX = file handle or drive number;
       *         CX = category/function or count;
       *         DX = data or buffer offset;
       *         DS:SI -> input buffer (for some subfunctions);
       *         ES:DI -> output buffer (for some subfunctions).
       * Output: CF=0 on success (output registers set by DosDevIOctl);
       *         CF=1, AX=error code on failure.
       *
       * Validates BP is in range 0x4400..0x44FF; if not, jumps to error_invalid.
       * Copies all relevant registers into a local lregs structure with AX=BP
       * (so DosDevIOctl sees the correct AH=44h, AL=subfunction), then calls
       * DosDevIOctl(). On failure, sets AX to the negated error code, updates
       * CritErrCode for non-device/access errors, and jumps to error_carry.
       * This is the kernel-internal IOCTL path used by internal callers.
       * Modifies: AX, FLAGS, CritErrCode; output registers per subfunction.
       */
      if (r.BP < 0x4400 || r.BP > 0x44ff)
        goto error_invalid;
      {
        lregs lr;
        lr.AX = r.BP;
        lr.BX = r.BX;
        lr.CX = r.CX;
        lr.DX = r.DX;
        lr.DI = r.DI;
        lr.SI = r.SI;
        lr.DS = r.DS;
        rc = DosDevIOctl(&lr);      /* can set critical error code! */
      }

      if (rc < SUCCESS)
      {
        r.AX = -rc;
        if (rc != DE_DEVICE && rc != DE_ACCESS)
          CritErrCode = r.AX;
        goto error_carry;
      }
      break;

    case 0x2c:
      /* Get pointer to second device driver in chain (skip NUL device).
       * Input:  none.
       * Output: BX:AX = far pointer (segment:offset) to the header of the
       *         second device driver in the device chain (i.e. nul_dev.dh_next),
       *         skipping the NUL device which is always first.
       *
       * Used by utilities such as Zip drive handlers that need to walk the
       * device chain starting from the second entry.
       */
      r.BX = FP_SEG(nul_dev.dh_next);
      r.AX = FP_OFF(nul_dev.dh_next);
      break;

    case 0x2d:
      /* Get extended error code (last critical error code).
       * Input:  none.
       * Output: AX = CritErrCode (the DOS extended error code from the most
       *         recent critical error or failed DOS call).
       *
       * Returns the current value of the global CritErrCode variable in AX.
       * This is the kernel-internal equivalent of the error-code portion of
       * INT 21h/AH=59h (Get Extended Error), used by internal callers that
       * only need the error code without the full class/action/locus detail.
       * CritErrCode is set by failed DOS calls and by the critical error
       * handler; it is cleared at the start of most INT 21h functions.
       */
      r.AX = CritErrCode;
      break;

    case 0x2e:
      /* Get or set error table addresses (ignored; stub for MS compatibility).
       * Input:  DL = subfunction (00h=get, 01h=set, 02h..05h select table);
       *         ES:DI -> new table pointer (for set subfunctions).
       * Output: ES:DI -> requested table pointer (for get subfunctions); CF=0.
       *
       * In a full implementation, reads or writes entries in the DOS-internal
       * error-message table-pointer array (four FAR pointers used by the
       * message-retrieval code that formats INT 21h/AH=59h descriptions).
       * FreeDOS currently ignores all subfunctions and returns immediately.
       * NOTE: MS DEBUG calls this with DS != DOSDS
       */
      break;

    case 0x2f:
      /* Set DOS version returned by INT 21h/AH=30h for all processes.
       * Input:  DX = new version to set (DL=major, DH=minor), or DX=0
       *         to reset to the emulated true DOS version.
       * Output: none.
       *         Modifies: os_setver_major, os_setver_minor.
       *
       * If DX is nonzero, sets os_setver_major = DL and os_setver_minor = DH,
       * causing INT 21h/AH=30h to return the specified version to all callers.
       * If DX is zero, resets os_setver_major/minor back to os_major/os_minor
       * (the kernel's true version). This duplicates the behavior of DOS 4
       * INT 2Fh/AX=122Fh and is also accessible via INT 21h/AH=33h/AL=FCh.
       * Used by version-faking utilities (e.g. SETVER equivalents).
       */
      if (r.DX)                /* set returned version from DX */
      {
        os_setver_major = r.DL;
        os_setver_minor = r.DH;
      }
      else                    /* set returned version from emulated true DOS version */
      {
        os_setver_major = os_major;
        os_setver_minor = os_minor;
      }
      break;

#if 0
    case 0x30:
      /* Windows 95: Find SFT entry by system file number (SFN).
       * Input:  BX = system file number (SFN, i.e. the raw index value
       *              stored in PSP.JFT[handle], NOT the handle itself).
       * Output: On success: ES:DI -> SFT entry; CF=0.
       *         On failure: CF=1, AX=0006h (invalid handle).
       *
       * Walk the SFT chain (anchor at SysVars+04h: link->count->entries).
       * Count down through each SFT block until the cumulative entry
       * index matches BX. This is equivalent to 0x16 but selected by
       * raw SFN rather than by JFT handle, bypassing the PSP/JFT
       * indirection layer. Used by Windows 95 kernel internals.
       */
      goto not_implemented;

    case 0x31:
      /* Windows 95: Set/clear "report Windows presence to DOS programs" flag.
       * Input:  DX = 0000h to enable Windows load (valid for one time only,
	   *                    added in Windows 95 OSR2 to protect FAT32 from
	   *                    the earlier Windows 95 versions); 
	   *         DX = 0001h to clear (hide Windows from INT 2Fh/AX=1600h);
	   *         DX = 0002h to set   (report Windows present).
       * Output: AX = 0, CF = 0 on success, AX = 1, CF = 1 on error.
       *
       * Store the low bit of BX into a DOS-internal byte flag
       * (WinReportFlag). This flag gates the response of the
       * INT 2Fh/AX=1600h Windows enhanced-mode installation check:
       * when clear, 1600h returns AL=0 (not present); when set, it
       * returns the Windows version. Return the previous value in AX.
       */
      goto not_implemented;
#endif

    default:
      if (r.AL <= 0x31)
      {
not_implemented:
        put_string("unimplemented internal dos function INT2F/12");
        put_unsigned(r.AL, 16, 2);
        put_string("\n");
        goto error_carry;
      }
  }
  return;
long_check:
  if (lrc >= SUCCESS)
  {
    r.AX = (UWORD)lrc;
    return;
  }
  rc = (int)lrc;
short_check:
  if (rc < SUCCESS)
    goto error_exit;
  return;
error_invalid:
  rc = DE_INVLDFUNC;
error_exit:
  r.AX = -rc;
  if (CritErrCode == SUCCESS)
    CritErrCode = r.AX;      /* Maybe set */
error_carry:
  r.FLAGS |= FLG_CARRY;

#undef r
}

/*
 * 2000/09/04  Brian Reifsnyder
 * Modified interrupts 0x25 & 0x26 to return more accurate error codes.
 */

