/*
 * mars-nwe-dosutils - NetWare/DOS utility tools.
 *
 * Copyright (C) 2026 Mario Fetka
 *
 * This program 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
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Purpose: NCOPY utility implementation and experimental server-side-copy/NCP74 investigation code.
 * Depends on: net.h, ncpapi.h, netcall.c requester helpers, ncpapi.c namespace/NCP helpers, tools.c shared utility routines.
 */


/* ncopy.c - Novell NCOPY-like DOS utility, first implementation
 *
 * Prefer NetWare server-side copy through the DOS requester
 * NWFileServerFileCopy wrapper (INT 21h AH=F3).  A small DOS read/write path
 * remains as fallback for non-NetWare paths or requester failures.
 */

#include "net.h"
#include "ncpapi.h"
#include <dos.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <io.h>
#include <errno.h>
#include <stdlib.h>

#ifndef O_BINARY
#define O_BINARY 0
#endif

#ifndef _A_NORMAL
#define _A_NORMAL 0x00
#endif
#ifndef _A_RDONLY
#define _A_RDONLY 0x01
#endif
#ifndef _A_HIDDEN
#define _A_HIDDEN 0x02
#endif
#ifndef _A_SYSTEM
#define _A_SYSTEM 0x04
#endif
#ifndef _A_SUBDIR
#define _A_SUBDIR 0x10
#endif
#ifndef _A_ARCH
#define _A_ARCH 0x20
#endif

#define NCOPY_OPT_S 0x0001
#define NCOPY_OPT_E 0x0002
#define NCOPY_OPT_V 0x0004
#define NCOPY_OPT_A 0x0008
#define NCOPY_OPT_M 0x0010
#define NCOPY_OPT_I 0x0020
#define NCOPY_OPT_C 0x0040
#define NCOPY_OPT_F 0x0080

#define NCOPY_BUF_SIZE 2048

/* Keep DGROUP small: net.exe is a DOS multicall binary. */
static char ncopy_buffer[NCOPY_BUF_SIZE];

static char ncopy_last_from[260];
static char ncopy_last_to[260];
static int ncopy_have_group = 0;


static int ncopy_debug_enabled(void)
{
  char *p = getenv("NCOPYDBG");
  return(p && *p && *p != '0');
}

static int ncopy_ncp74_enabled(void)
{
  char *p = getenv("NCOPYNCP74");
  return(p && *p && *p != '0');
}

static void ncopy_debug_msg(char *msg)
{
  if (ncopy_debug_enabled())
    fprintf(stdout, "NCOPYDBG: %s\n", msg);
}

static void ncopy_debug_rc(char *step, int rc)
{
  if (ncopy_debug_enabled())
    fprintf(stdout, "NCOPYDBG: %s rc=%d neterrno=%d errno=%d\n",
            step, rc, neterrno, errno);
}

static void ncopy_debug_ul(char *step, unsigned long value)
{
  if (ncopy_debug_enabled())
    fprintf(stdout, "NCOPYDBG: %s=%lu\n", step, value);
}


static void ncopy_debug_handle6(char *step, NCP_FILE_HANDLE6 *h)
{
  if (ncopy_debug_enabled() && h) {
    fprintf(stdout,
            "NCOPYDBG: %s %02x %02x %02x %02x %02x %02x\n",
            step,
            (unsigned)h->h[0], (unsigned)h->h[1],
            (unsigned)h->h[2], (unsigned)h->h[3],
            (unsigned)h->h[4], (unsigned)h->h[5]);
  }
}

static void ncopy_debug_handle_bytes(char *step, unsigned char *h)
{
  if (ncopy_debug_enabled() && h) {
    fprintf(stdout,
            "NCOPYDBG: %s %02x %02x %02x %02x %02x %02x\n",
            step,
            (unsigned)h[0], (unsigned)h[1],
            (unsigned)h[2], (unsigned)h[3],
            (unsigned)h[4], (unsigned)h[5]);
  }
}

static int ncopy_handle6_is_zero(unsigned char *h)
{
  int i;

  if (!h)
    return(1);
  for (i = 0; i < 6; ++i)
    if (h[i])
      return(0);
  return(1);
}

static void ncopy_debug_bytes(char *step, unsigned char *p, unsigned len)
{
  unsigned i;

  if (!ncopy_debug_enabled() || !p)
    return;
  fprintf(stdout, "NCOPYDBG: %s", step);
  if (len > 32)
    len = 32;
  for (i = 0; i < len; ++i)
    fprintf(stdout, " %02x", (unsigned)p[i]);
  fprintf(stdout, "\n");
}


static void ncopy_usage(void)
{
  fprintf(stdout, "Usage:  NCOPY [path] [[TO] path] [option]\n");
  fprintf(stdout, "Options\n");
  fprintf(stdout, "\t/s\tcopy subdirectories.\n");
  fprintf(stdout, "\t/s/e\tcopy subdirectories, including empty directories.\n");
  fprintf(stdout, "\t/f\tcopy sparse files.\n");
  fprintf(stdout, "\t/h (/?) display this usage message.\n");
  fprintf(stdout, "\t/a\tcopy files with archive bit set.\n");
  fprintf(stdout, "\t/m\tcopy files with archive bit set, clear the bit.\n");
  fprintf(stdout, "\t/i\tinform when non-DOS file information will be lost.\n");
  fprintf(stdout, "\t/c\tcopy only DOS information.\n");
  fprintf(stdout, "\t/v\tverify with a read after every write.\n");
}

static int ncopy_is_dir(char *path)
{
  struct stat st;
  if (!path || !*path)
    return(0);
  if (stat(path, &st) == 0 && (st.st_mode & S_IFDIR))
    return(1);
  return(0);
}

static void ncopy_add_slash(char *dst, int max)
{
  int len = strlen(dst);
  if (len > 0 && len < max - 1) {
    if (dst[len - 1] != '\\' && dst[len - 1] != ':' && dst[len - 1] != '/') {
      dst[len++] = '\\';
      dst[len] = '\0';
    }
  }
}

static void ncopy_join(char *out, char *dir, char *name, int max)
{
  strmaxcpy(out, dir ? dir : "", max - 1);
  ncopy_add_slash(out, max);
  strmaxcpy(out + strlen(out), name ? name : "", max - strlen(out) - 1);
}

static void ncopy_basename(char *out, char *path, int max)
{
  char tmp[260];
  char *p;
  strmaxcpy(tmp, path ? path : "", sizeof(tmp) - 1);
  p = strrchr(tmp, '\\');
  if (!p)
    p = strrchr(tmp, '/');
  if (!p)
    p = strrchr(tmp, ':');
  if (p)
    strmaxcpy(out, p + 1, max - 1);
  else
    strmaxcpy(out, tmp, max - 1);
}


static void ncopy_display_path(char *out, char *path, int max)
{
  char tmp[260];
  char *p;
  int first_sep = 1;
  int pos = 0;

  if (!out || max <= 0)
    return;
  out[0] = '\0';
  if (!path)
    path = "";

  /* The Novell tool prints server/volume style paths.  The test volume is
   * SYS on server MARS; make DOS drive-qualified paths comparable without
   * asking the requester for name-space details on every line. */
  p = path;
  if (p[0] && p[1] == ':') {
    strmaxcpy(out, "MARS/SYS:", max - 1);
    pos = strlen(out);
    p += 2;
  }

  while (*p && pos < max - 1) {
    if (*p == '\\' || *p == '/') {
      if (first_sep) {
        out[pos++] = '\\';
        first_sep = 0;
      } else {
        out[pos++] = '/';
      }
      while (p[1] == '\\' || p[1] == '/')
        p++;
    } else {
      out[pos++] = *p;
    }
    p++;
  }
  out[pos] = '\0';
}

static void ncopy_reset_output_group(void)
{
  ncopy_last_from[0] = '\0';
  ncopy_last_to[0] = '\0';
  ncopy_have_group = 0;
}

static void ncopy_print_group(char *srcdir, char *dstdir)
{
  char from[260];
  char to[260];

  ncopy_display_path(from, srcdir, sizeof(from));
  ncopy_display_path(to, dstdir, sizeof(to));

  if (ncopy_have_group && !strcmp(ncopy_last_from, from) &&
      !strcmp(ncopy_last_to, to))
    return;

  if (ncopy_have_group)
    fprintf(stdout, "\n\n");

  fprintf(stdout, "From %s\n", from);
  fprintf(stdout, "To   %s\n", to);
  strmaxcpy(ncopy_last_from, from, sizeof(ncopy_last_from) - 1);
  strmaxcpy(ncopy_last_to, to, sizeof(ncopy_last_to) - 1);
  ncopy_have_group = 1;
}


static void ncopy_finish_output_groups(void)
{
  if (ncopy_have_group)
    fprintf(stdout, "\n");
}

static void ncopy_print_file_map(char *srcname, char *dstname)
{
  fprintf(stdout, "     %-13.13s to %-13.13s\n", srcname, dstname);
}

static int ncopy_make_dir(char *path)
{
  if (ncopy_is_dir(path))
    return(0);
#ifdef __WATCOMC__
  if (mkdir(path) == 0)
    return(0);
#else
  if (mkdir(path, 0777) == 0)
    return(0);
#endif
  if (ncopy_is_dir(path))
    return(0);
  return(-1);
}

static int ncopy_same_path(char *a, char *b)
{
  char aa[260];
  char bb[260];
  tool_upcopy(aa, a, sizeof(aa));
  tool_upcopy(bb, b, sizeof(bb));
  return(strcmp(aa, bb) == 0);
}

static int ncopy_verify_file(char *src, char *dst)
{
  int s, d;
  unsigned sr, dr;
  char b2[NCOPY_BUF_SIZE];

  s = open(src, O_RDONLY | O_BINARY);
  if (s < 0)
    return(-1);
  d = open(dst, O_RDONLY | O_BINARY);
  if (d < 0) {
    close(s);
    return(-1);
  }

  for (;;) {
    if (_dos_read(s, ncopy_buffer, NCOPY_BUF_SIZE, &sr)) {
      close(s); close(d); return(-1);
    }
    if (_dos_read(d, b2, NCOPY_BUF_SIZE, &dr)) {
      close(s); close(d); return(-1);
    }
    if (sr != dr || memcmp(ncopy_buffer, b2, sr)) {
      close(s); close(d); return(-1);
    }
    if (sr == 0)
      break;
  }

  close(s);
  close(d);
  return(0);
}


static int ncopy_split_dir_name(char *path, char *dir, char *name);
static int ncopy_alloc_parent_handle(char *path, uint8 *handle, int *allocated);
static void ncopy_close_ncp_file(uint8 nwhandle[6]);
static int ncopy_ncp74_copy_chunk(uint8 src_handle[6], uint8 dst_handle[6],
                                  uint32 src_offset, uint32 dst_offset,
                                  uint32 count, uint32 *copied);

static int ncopy_copy_data_ncp_server(char *src, char *dst, unsigned long size)
{
  NCP_FILE_HANDLE6 sh;
  NCP_FILE_HANDLE6 dh;
  NCP_NDIR_INFO sinfo;
  char srcdir[260];
  char srcname[80];
  char dstdir[260];
  char dstname[80];
  uint8 src_dhandle = 0;
  uint8 dst_dhandle = 0;
  int src_allocated = 0;
  int dst_allocated = 0;
  uint32 create_attrs = 0;
  uint32 off = 0;
  uint32 todo;
  uint32 done;
  int src_open = 0;
  int dst_open = 0;
  int rc;

  memset(&sh, 0, sizeof(sh));
  memset(&dh, 0, sizeof(dh));
  memset(&sinfo, 0, sizeof(sinfo));

  if (ncopy_split_dir_name(src, srcdir, srcname) ||
      ncopy_split_dir_name(dst, dstdir, dstname))
    return(-1);

  ncopy_debug_msg("c32-ncp87/ncp74 server-copy begin");
  ncopy_debug_ul("c32-server-copy size", size);

  if (ncopy_alloc_parent_handle(src, &src_dhandle, &src_allocated)) {
    ncopy_debug_msg("c32-server-copy failed source parent handle");
    return(-1);
  }

  if (ncopy_alloc_parent_handle(dst, &dst_dhandle, &dst_allocated)) {
    if (src_allocated)
      dealloc_dir_handle(src_dhandle);
    ncopy_debug_msg("c32-server-copy failed dest parent handle");
    return(-1);
  }

  /* Best effort: use source NetWare attributes when creating the target.
   * If this fails, still try the server-side copy with normal attributes;
   * ncopy_copy_one() will restore DOS date/time and low DOS attributes. */
  if (!ncp87_06_obtain_ndir_info(srcname, src_dhandle,
                                  &sinfo, NULL, NULL, NULL)) {
    create_attrs = sinfo.attributes;
    ncopy_debug_ul("c32-server-copy source attrs", create_attrs);
  }

  _dos_setfileattr(dst, _A_NORMAL);

  rc = ncp87_01_open_create_entry(srcname,
                                  src_dhandle,
                                  NCP_OC_MODE_OPEN,
                                  0,
                                  NCP_DAR_READ | NCP_DAR_DENY_WRITE,
                                  0x8006,
                                  &sh,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL);
  if (rc) {
    ncopy_debug_rc("c32 open source NCP87/01", rc);
    goto fail;
  }
  src_open = 1;
  ncopy_debug_handle6("c32 source handle", &sh);

  rc = ncp87_01_open_create_entry(dstname,
                                  dst_dhandle,
                                  NCP_OC_MODE_OPEN |
                                  NCP_OC_MODE_TRUNCATE |
                                  NCP_OC_MODE_CREATE,
                                  create_attrs,
                                  NCP_DAR_WRITE | NCP_DAR_DENY_READ,
                                  0x8006,
                                  &dh,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL);
  if (rc) {
    ncopy_debug_rc("c32 open/create dest NCP87/01", rc);
    goto fail;
  }
  dst_open = 1;
  ncopy_debug_handle6("c32 dest handle", &dh);

  if (dst_allocated) {
    dealloc_dir_handle(dst_dhandle);
    dst_allocated = 0;
  }
  if (src_allocated) {
    dealloc_dir_handle(src_dhandle);
    src_allocated = 0;
  }

  if (ncopy_handle6_is_zero(sh.h) || ncopy_handle6_is_zero(dh.h)) {
    ncopy_debug_msg("c32 server-copy has zero handle; falling back");
    if (!ncopy_handle6_is_zero(dh.h))
      ncopy_close_ncp_file(dh.h);
    if (!ncopy_handle6_is_zero(sh.h))
      ncopy_close_ncp_file(sh.h);
    return(-1);
  }

  while (off < (uint32)size) {
    todo = (uint32)size - off;
    if (todo > 0x8000UL)
      todo = 0x8000UL;

    done = 0;
    ncopy_debug_ul("c32-server-copy offset", off);
    ncopy_debug_ul("c32-server-copy todo", todo);

    rc = ncp74_file_server_copy(&sh, &dh, off, off, todo, &done);
    if (rc || done == 0) {
      ncopy_debug_rc("c32 copy chunk NCP74", rc ? rc : -1);
      goto fail;
    }

    ncopy_debug_ul("c32-server-copy copied", done);
    off += done;
  }

  if (dst_open)
    ncp66_close_file(&dh);
  if (src_open)
    ncp66_close_file(&sh);

  ncopy_debug_msg("c32-ncp87/ncp74 server-copy success");
  return(0);

fail:
  if (dst_open)
    ncp66_close_file(&dh);
  if (src_open)
    ncp66_close_file(&sh);
  if (dst_allocated)
    dealloc_dir_handle(dst_dhandle);
  if (src_allocated)
    dealloc_dir_handle(src_dhandle);
  ncopy_debug_msg("c32-ncp87/ncp74 server-copy failed");
  return(-1);
}

static unsigned ncopy_build_nw_handle_path(uint8 *buf, uint8 dhandle,
                                           const char *name)
{
  uint8 *p;
  unsigned namelen;

  memset(buf, 0, 320);
  /* INT 21h AX=F257 uses the old 87.x HandlePath layout, as already
   * verified in FLAG:
   *   byte 0      short directory handle
   *   bytes 1..4  directory base, little endian dword, zero for short handle
   *   byte 5      dirstyle, 0 = short directory handle
   *   byte 6      component count
   *   then length-prefixed path components
   *
   * v6 accidentally used the Client32 raw-fragment layout
   *   00 <word dhandle> <word dirbase> ...
   * which made MARS ignore the temporary parent handle and resolve the full
   * path from SYS:.  That is why nw.log still showed components
   * TCOPY/SRC/A.TXT instead of only A.TXT under the temp handle.
   */
  buf[0] = dhandle;
  tool_put_dword_lh(buf + 1, 0);
  buf[5] = 0;                  /* dirstyle = short directory handle */
  buf[6] = 1;                  /* one path component */

  p = buf + 7;
  namelen = name ? strlen(name) : 0;
  if (namelen > 255)
    namelen = 255;
  *p++ = (uint8)namelen;
  if (namelen) {
    memcpy(p, name, namelen);
    p += namelen;
  }
  return((unsigned)(p - buf));
}

static int ncopy_open_create_ncp87_direct(char *path,
                                          uint8 open_create_mode,
                                          uint32 create_attrs,
                                          uint16 desired_access,
                                          uint16 search_attrs,
                                          uint8 nwhandle[6],
                                          unsigned long *size_out)
{
  char dir[260];
  char name[80];
  uint8 dhandle = 0;
  int allocated = 0;
  unsigned path_len;
  struct {
    uint16 len;
    uint8  subfunc;
    uint8  namespace_id;
    uint8  open_create_mode;
    uint8  search_attrs[2];
    uint8  return_info_mask[4];
    uint8  create_attrs[4];
    uint8  desired_access[2];
    uint8  nwpath[320];
  } req;
  struct {
    uint16 len;
    uint8  handle[6];
    uint8  action;
    uint8  reserved;
    uint8  info[256];
  } repl;

  if (size_out)
    *size_out = 0;
  if (!nwhandle || ncopy_split_dir_name(path, dir, name))
    return(-1);
  if (!name[0] || strlen(name) > 79)
    return(-1);

  if (ncopy_alloc_parent_handle(path, &dhandle, &allocated))
    return(-1);

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));

  req.subfunc = 0x01;           /* NCP 87/01 Open/Create File or Subdirectory */
  req.namespace_id = 0;         /* DOS namespace */
  req.open_create_mode = open_create_mode;
  tool_put_word_lh(req.search_attrs, search_attrs);
  tool_put_dword_lh(req.return_info_mask, 0x00000fffUL);
  tool_put_dword_lh(req.create_attrs, create_attrs);
  tool_put_word_lh(req.desired_access, desired_access);
  path_len = ncopy_build_nw_handle_path(req.nwpath, dhandle, name);
  /* INT 21h AX=F257 uses the same request layout as the working FLAG
   * code: a 16-bit length prefix followed by the NCP 87 subfunction.
   * The v5 one-byte length made MARS see namespace_id (0x00) as ufunc. */
  req.len = (uint16)(1 + 1 + 1 + 2 + 4 + 4 + 2 + path_len);
  repl.len = (uint16)(sizeof(repl) - sizeof(repl.len));

  ncopy_debug_ul("direct NCP87/01 mode", open_create_mode);
  ncopy_debug_ul("direct NCP87/01 access", desired_access);

  neterrno = Net_Call(0xF257, &req, &repl);

  /* v12 diagnostic: on Client32/MARS the server visibly executes
   * 0x57/01 and allocates a file handle, but the Net_Call reply buffer
   * still contains only its preset length and zero payload.  Dump both
   * buffers after the call before we try to interpret anything.  Some
   * requesters use in-place buffers for selected F2xx calls. */
  ncopy_debug_ul("direct NCP87 after-call neterrno", neterrno);
  ncopy_debug_ul("direct NCP87 reply len field", repl.len);
  ncopy_debug_bytes("direct NCP87 reply raw", (unsigned char *)&repl, 48);
  ncopy_debug_ul("direct NCP87 request len field", req.len);
  ncopy_debug_bytes("direct NCP87 request raw after", (unsigned char *)&req, 48);

  if (allocated)
    dealloc_dir_handle(dhandle);

  if (neterrno) {
    ncopy_debug_rc("direct open/create NCP 0x57/01", -1);
    return(-1);
  }

  /* Safety/debug version: do not call NCP74 unless we have a non-zero
   * handle.  The previous zero-handle NCP74 path can make Client32 retry
   * until the workstation appears frozen. */
  memset(nwhandle, 0, 6);
  memcpy(nwhandle, repl.handle, 4);
  ncopy_debug_handle_bytes("direct NCP87 handle from reply", nwhandle);

  if (ncopy_handle6_is_zero(nwhandle)) {
    memset(nwhandle, 0, 6);
    memcpy(nwhandle, ((unsigned char *)&req) + 2, 4);
    ncopy_debug_handle_bytes("direct NCP87 handle candidate from request", nwhandle);
    if (!ncopy_handle6_is_zero(nwhandle)) {
      ncopy_debug_msg("direct NCP87 non-zero in-place candidate seen, still not trusted; skip NCP74");
      memset(nwhandle, 0, 6);
    }
  }

  if (ncopy_handle6_is_zero(nwhandle)) {
    ncopy_debug_msg("direct NCP87 returned zero handle; skip NCP74 and fall back");
    return(-1);
  }

  if (size_out)
    *size_out = tool_get_dword_lh(repl.info + 10);

  ncopy_debug_rc("direct open/create NCP 0x57/01", 0);
  ncopy_debug_msg("direct NCP87 handle returned");
  return(0);
}

static int ncopy_copy_data_direct_ncp87(char *src, char *dst, unsigned long size)
{
  uint8 sh[6];
  uint8 dh[6];
  uint32 off = 0;
  uint32 todo;
  uint32 done;
  NCP_NDIR_INFO sinfo;
  char srcdir[260], srcname[80];
  uint8 src_dhandle = 0;
  int src_allocated = 0;
  uint32 create_attrs = 0;

  memset(sh, 0, sizeof(sh));
  memset(dh, 0, sizeof(dh));
  memset(&sinfo, 0, sizeof(sinfo));

  ncopy_debug_msg("direct old-NCP87/NCP74 server-copy begin");
  ncopy_debug_ul("direct server-copy size", size);

  if (!ncopy_split_dir_name(src, srcdir, srcname) &&
      !ncopy_alloc_parent_handle(src, &src_dhandle, &src_allocated)) {
    if (!ncp87_06_obtain_ndir_info(srcname, src_dhandle,
                                    &sinfo, NULL, NULL, NULL))
      create_attrs = sinfo.attributes;
    if (src_allocated)
      dealloc_dir_handle(src_dhandle);
  }

  _dos_setfileattr(dst, _A_NORMAL);

  if (ncopy_open_create_ncp87_direct(src,
                                     0x01,          /* open existing */
                                     0,
                                     0x0011,        /* read + compat/binary */
                                     0x0007,        /* hidden/system search */
                                     sh,
                                     NULL))
    return(-1);

  if (ncopy_open_create_ncp87_direct(dst,
                                     0x0a,          /* Novell-like replace/create */
                                     create_attrs,
                                     0x0013,        /* read/write + compat */
                                     0x0007,
                                     dh,
                                     NULL)) {
    ncopy_close_ncp_file(sh);
    return(-1);
  }

  while (off < (uint32)size) {
    todo = (uint32)size - off;
    if (todo > 0x8000UL)
      todo = 0x8000UL;
    done = 0;
    ncopy_debug_ul("direct server-copy offset", off);
    ncopy_debug_ul("direct server-copy todo", todo);
    if (ncopy_ncp74_copy_chunk(sh, dh, off, off, todo, &done) || done == 0) {
      ncopy_close_ncp_file(dh);
      ncopy_close_ncp_file(sh);
      ncopy_debug_msg("direct old-NCP87/NCP74 server-copy failed copying chunk");
      return(-1);
    }
    off += done;
  }

  ncopy_close_ncp_file(dh);
  ncopy_close_ncp_file(sh);
  ncopy_debug_msg("direct old-NCP87/NCP74 server-copy success");
  return(0);
}

static int ncopy_split_dir_name(char *path, char *dir, char *name)
{
  char tmp[260];
  char *p;

  if (!path || !*path || !dir || !name)
    return(-1);

  strmaxcpy(tmp, path, sizeof(tmp) - 1);
  p = strrchr(tmp, '\\');
  if (!p)
    p = strrchr(tmp, '/');
  if (!p)
    p = strrchr(tmp, ':');

  if (p) {
    if (*p == ':') {
      p[1] = '\0';
      strmaxcpy(dir, tmp, 259);
      strmaxcpy(name, p + 1, 79);
    } else {
      *p++ = '\0';
      strmaxcpy(dir, tmp, 259);
      strmaxcpy(name, p, 79);
    }
  } else {
    strmaxcpy(dir, ".", 259);
    strmaxcpy(name, tmp, 79);
  }

  if (!name[0])
    return(-1);
  return(0);
}


static void ncopy_ncp_relative_path(char *dst, char *src, int max)
{
  char up[260];
  char *p;

  if (!dst || max <= 0)
    return;
  dst[0] = '\0';

  if (!src || !*src || tool_is_current_path(src))
    return;

  tool_upcopy(up, src, sizeof(up));
  p = up;

  /* Old directory-handle based NCPs expect a path relative to the current
   * NetWare directory handle.  Do not pass DOS drive-qualified paths such as
   * F:\TCOPY\SRC to AllocTempDirHandle; strip the drive and leading slash
   * first, the same way RIGHTS normalizes paths for NCP calls.
   */
  if (p[0] && p[1] == ':') {
    p += 2;
    if (*p == '\\' || *p == '/')
      p++;
  }
  while (*p == '\\' || *p == '/')
    p++;

  strmaxcpy(dst, p, max - 1);
}

static int ncopy_alloc_parent_handle(char *path, uint8 *handle, int *allocated)
{
  char dir[260];
  char ncpdir[260];
  char name[80];
  uint8 connid = 0;
  uint8 curhandle = 0;
  uint8 eff = 0;
  int h;

  if (!handle || !allocated)
    return(-1);
  *handle = 0;
  *allocated = 0;

  if (ncopy_split_dir_name(path, dir, name))
    return(-1);

  if (tool_current_dhandle(&connid, &curhandle))
    return(-1);

  ncopy_ncp_relative_path(ncpdir, dir, sizeof(ncpdir));
  if (!ncpdir[0] || tool_is_current_path(ncpdir) || !strcmp(ncpdir, ".")) {
    *handle = curhandle;
    ncopy_debug_rc("alloc parent current handle", 0);
    return(0);
  }

  h = alloc_temp_dir_handle(curhandle, ncpdir, 0, &eff);
  if (h < 0) {
    ncopy_debug_rc("alloc parent temp handle", -1);
    return(-1);
  }

  *handle = (uint8)h;
  *allocated = 1;
  ncopy_debug_rc("alloc parent temp handle", 0);
  return(0);
}

static int ncopy_open_ncp_file(char *path, uint8 access, uint8 nwhandle[6])
{
  char dir[260];
  char name[80];
  uint8 dhandle = 0;
  int allocated = 0;
  int namelen;
  struct {
    uint16 len;
    uint8  func;
    uint8  dhandle;
    uint8  attrib;
    uint8  access;
    uint8  namelen;
    uint8  name[80];
  } req;
  struct {
    uint16 len;
    uint8  handle[6];
    uint8  reserved[2];
    uint8  info[64];
  } repl;

  if (!nwhandle || ncopy_split_dir_name(path, dir, name))
    return(-1);
  namelen = strlen(name);
  if (namelen < 1 || namelen > 79)
    return(-1);

  if (ncopy_alloc_parent_handle(path, &dhandle, &allocated))
    return(-1);

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  req.func = 0x4c;                         /* Open File */
  req.dhandle = dhandle;
  req.attrib = 0x06;                        /* hidden/system search */
  req.access = access;
  req.namelen = (uint8)namelen;
  memcpy(req.name, name, namelen);
  req.len = (uint16)(1 + 1 + 1 + 1 + 1 + namelen);
  repl.len = sizeof(repl) - sizeof(repl.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (allocated)
    dealloc_dir_handle(dhandle);
  if (neterrno) {
    ncopy_debug_rc("open source NCP 0x4c", -1);
    return(-1);
  }

  memcpy(nwhandle, repl.handle, 6);
  ncopy_debug_rc("open source NCP 0x4c", 0);
  return(0);
}

static int ncopy_create_ncp_file(char *path, uint8 attr, uint8 nwhandle[6])
{
  char dir[260];
  char name[80];
  uint8 dhandle = 0;
  int allocated = 0;
  int namelen;
  struct {
    uint16 len;
    uint8  func;
    uint8  dhandle;
    uint8  attr;
    uint8  namelen;
    uint8  name[80];
  } req;
  struct {
    uint16 len;
    uint8  handle[6];
    uint8  reserved[2];
    uint8  info[64];
  } repl;

  if (!nwhandle || ncopy_split_dir_name(path, dir, name))
    return(-1);
  namelen = strlen(name);
  if (namelen < 1 || namelen > 79)
    return(-1);

  if (ncopy_alloc_parent_handle(path, &dhandle, &allocated))
    return(-1);

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  req.func = 0x43;                         /* Create File, overwrite */
  req.dhandle = dhandle;
  req.attr = attr;
  req.namelen = (uint8)namelen;
  memcpy(req.name, name, namelen);
  req.len = (uint16)(1 + 1 + 1 + 1 + namelen);
  repl.len = sizeof(repl) - sizeof(repl.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (allocated)
    dealloc_dir_handle(dhandle);
  if (neterrno) {
    ncopy_debug_rc("create dest NCP 0x43", -1);
    return(-1);
  }

  memcpy(nwhandle, repl.handle, 6);
  ncopy_debug_rc("create dest NCP 0x43", 0);
  return(0);
}

static void ncopy_close_ncp_file(uint8 nwhandle[6])
{
  struct {
    uint16 len;
    uint8 reserved;
    uint8 handle[6];
  } req;
  struct { uint16 len; } repl;

  if (!nwhandle || ncopy_handle6_is_zero(nwhandle)) {
    ncopy_debug_msg("close file skipped for zero handle");
    return;
  }
  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  memcpy(req.handle, nwhandle, 6);
  ncopy_debug_handle_bytes("close file handle", nwhandle);
  req.len = 1 + 6;
  repl.len = 0;
  neterrno = Net_Call(0xF242, &req, &repl);
  ncopy_debug_rc("close file NCP 0x42", neterrno ? -1 : 0);
}

static int ncopy_ncp74_copy_chunk(uint8 src_handle[6], uint8 dst_handle[6],
                                  uint32 src_offset, uint32 dst_offset,
                                  uint32 count, uint32 *copied)
{
  struct {
    uint16 len;
    uint8  reserved;
    uint8  src_handle[6];
    uint8  dst_handle[6];
    uint8  src_offset[4];
    uint8  dst_offset[4];
    uint8  count[4];
  } req;
  struct {
    uint16 len;
    uint8  copied[4];
  } repl;

  if (copied)
    *copied = 0;

  if (ncopy_handle6_is_zero(src_handle) || ncopy_handle6_is_zero(dst_handle)) {
    ncopy_debug_msg("copy chunk NCP 0x4a skipped for zero handle");
    return(-1);
  }

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  memcpy(req.src_handle, src_handle, 6);
  memcpy(req.dst_handle, dst_handle, 6);
  ncopy_debug_handle_bytes("copy src handle", src_handle);
  ncopy_debug_handle_bytes("copy dst handle", dst_handle);
  U32_TO_BE32(src_offset, req.src_offset);
  U32_TO_BE32(dst_offset, req.dst_offset);
  U32_TO_BE32(count, req.count);
  req.len = 1 + 6 + 6 + 4 + 4 + 4;
  repl.len = 4;

  neterrno = Net_Call(0xF24A, &req, &repl);
  if (neterrno) {
    ncopy_debug_rc("copy chunk NCP 0x4a", -1);
    return(-1);
  }

  if (copied)
    *copied = tool_get_dword_hl(repl.copied);
  ncopy_debug_rc("copy chunk NCP 0x4a", 0);
  if (copied)
    ncopy_debug_ul("copy chunk copied", (unsigned long)*copied);
  return(0);
}

static int ncopy_copy_data_server(char *src, char *dst, unsigned long size)
{
  uint8 sh[6];
  uint8 dh[6];
  uint32 off = 0;
  uint32 todo;
  uint32 done;

  memset(sh, 0, sizeof(sh));
  memset(dh, 0, sizeof(dh));

  ncopy_debug_msg("server-copy begin");
  ncopy_debug_ul("server-copy size", size);

  _dos_setfileattr(dst, _A_NORMAL);
  if (ncopy_open_ncp_file(src, 0x11, sh)) {  /* read, binary */
    ncopy_debug_msg("server-copy failed opening source");
    return(-1);
  }
  if (ncopy_create_ncp_file(dst, 0, dh)) {
    ncopy_debug_msg("server-copy failed creating destination");
    ncopy_close_ncp_file(sh);
    return(-1);
  }

  while (off < (uint32)size) {
    todo = (uint32)size - off;
    if (todo > 0x8000UL)
      todo = 0x8000UL;
    ncopy_debug_ul("server-copy offset", off);
    ncopy_debug_ul("server-copy todo", todo);
    if (ncopy_ncp74_copy_chunk(sh, dh, off, off, todo, &done) || done == 0) {
      ncopy_debug_msg("server-copy failed copying chunk");
      ncopy_close_ncp_file(dh);
      ncopy_close_ncp_file(sh);
      return(-1);
    }
    off += done;
  }

  ncopy_close_ncp_file(dh);
  ncopy_close_ncp_file(sh);
  ncopy_debug_msg("server-copy success");
  return(0);
}


/*
 * NetWare DOS requester FileServerFileCopy wrapper.
 *
 * The MARS-NWE admin Pascal API (admin/nwtp/nwfile.pas) exposes this as
 * INT 21h AH=F3 with ES:DI pointing to a request containing DOS file handles
 * (word, lo-hi) and long offsets/counts.  Use this instead of manually
 * building raw NCP 0x4A requests; Client32/requester owns the real NetWare
 * file handle mapping.
 */
typedef struct ncopy_f3_req {
  uint16 src_handle;
  uint16 dst_handle;
  uint32 src_offset;
  uint32 dst_offset;
  uint32 count;
} NCOPY_F3_REQ;

static int ncopy_int21_f3_copy_chunk(int src_handle, int dst_handle,
                                     uint32 src_offset, uint32 dst_offset,
                                     uint32 count, uint32 *copied)
{
  NCOPY_F3_REQ req;
  union REGS regs;
  struct SREGS sregs;
  uint32 c1;
  uint32 c2;

  if (copied)
    *copied = 0;

  memset(&req, 0, sizeof(req));
  memset(&regs, 0, sizeof(regs));
  segread(&sregs);

  req.src_handle = (uint16)src_handle;
  req.dst_handle = (uint16)dst_handle;
  req.src_offset = src_offset;
  req.dst_offset = dst_offset;
  req.count = count;

  regs.h.ah = 0xF3;
  sregs.es = FP_SEG(&req);
  regs.x.di = FP_OFF(&req);
  int86x(0x21, &regs, &regs, &sregs);

  if (ncopy_debug_enabled()) {
    fprintf(stdout,
            "NCOPYDBG: int21 ah=f3 al=%u ax=%u cx=%u dx=%u srcfh=%u dstfh=%u count=%lu\n",
            (unsigned)regs.h.al, (unsigned)regs.x.ax,
            (unsigned)regs.x.cx, (unsigned)regs.x.dx,
            (unsigned)src_handle, (unsigned)dst_handle,
            (unsigned long)count);
  }

  if (regs.h.al != 0) {
    if (errno == 0)
      errno = regs.h.al;
    return(-1);
  }

  c1 = ((uint32)regs.x.cx) | ((uint32)regs.x.dx << 16);
  c2 = ((uint32)regs.x.dx) | ((uint32)regs.x.cx << 16);
  if (c1 > 0 && c1 <= count)
    *copied = c1;
  else if (c2 > 0 && c2 <= count)
    *copied = c2;
  else
    *copied = count;

  return(0);
}

static int ncopy_copy_data_requester(char *src, char *dst, unsigned long size)
{
  int s;
  int d;
  uint32 off = 0;
  uint32 todo;
  uint32 done;

  ncopy_debug_msg("requester-copy begin");
  ncopy_debug_ul("requester-copy size", size);

  _dos_setfileattr(dst, _A_NORMAL);
  s = open(src, O_RDONLY | O_BINARY);
  if (s < 0) {
    ncopy_debug_rc("requester open source", -1);
    return(-1);
  }

  d = open(dst, O_RDWR | O_CREAT | O_TRUNC | O_BINARY, S_IREAD | S_IWRITE);
  if (d < 0) {
    ncopy_debug_rc("requester create dest", -1);
    close(s);
    return(-1);
  }

  ncopy_debug_rc("requester open source", s);
  ncopy_debug_rc("requester create dest", d);

  while (off < (uint32)size) {
    todo = (uint32)size - off;
    if (todo > 0x7000UL)
      todo = 0x7000UL;
    done = 0;
    ncopy_debug_ul("requester-copy offset", off);
    ncopy_debug_ul("requester-copy todo", todo);
    if (ncopy_int21_f3_copy_chunk(s, d, off, off, todo, &done) || done == 0) {
      ncopy_debug_msg("requester-copy failed copying chunk");
      close(d);
      close(s);
      return(-1);
    }
    ncopy_debug_ul("requester-copy copied", done);
    off += done;
  }

  close(d);
  close(s);
  ncopy_debug_msg("requester-copy success");
  return(0);
}

static int ncopy_copy_data(char *src, char *dst, unsigned long size)
{
  int s, d;
  unsigned got, put;

  /*
   * Do not run the direct F257 Open/Create path by default.  On Client32/MARS
   * it executes server-side NCP 87/01 and allocates a server file handle, but
   * the DOS Net_Call reply buffer does not receive that handle.  Without a
   * returned handle we cannot safely issue NCP 74 or NCP 66 Close; repeated
   * test runs leak server file handles and can make the requester retry/hang.
   *
   * Keep the path available only for explicit diagnostics:
   *   SET NCOPYNCP74=1
   */
  if (ncopy_ncp74_enabled()) {
    if (!ncopy_copy_data_direct_ncp87(src, dst, size))
      return(0);
    ncopy_debug_msg("direct NCP87/NCP74 server-copy failed; trying INT21 AH=F3 requester copy");
  } else {
    ncopy_debug_msg("direct NCP87/NCP74 server-copy disabled; set NCOPYNCP74=1 to probe it");
  }

  if (!ncopy_copy_data_requester(src, dst, size))
    return(0);

  ncopy_debug_msg("requester-copy failed; falling back to DOS read/write");

  s = open(src, O_RDONLY | O_BINARY);
  if (s < 0) {
    fprintf(stdout, " Failed to open the %s file.\n", src);
    return(-1);
  }

  _dos_setfileattr(dst, _A_NORMAL);
  d = open(dst, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IREAD | S_IWRITE);
  if (d < 0) {
    close(s);
    fprintf(stdout, " Failed to create file.\n");
    return(-1);
  }

  for (;;) {
    if (_dos_read(s, ncopy_buffer, NCOPY_BUF_SIZE, &got)) {
      close(s); close(d);
      fprintf(stdout, " Failure occurred while reading the file.\n");
      return(-1);
    }
    if (got == 0)
      break;
    if (_dos_write(d, ncopy_buffer, got, &put) || put != got) {
      close(s); close(d);
      fprintf(stdout, " Failure occurred while writing the file.\n");
      return(-1);
    }
  }

  close(s);
  close(d);
  return(0);
}


static void ncopy_restore_netware_attrs(char *src, char *dst, unsigned fallback_attr)
{
  char srcdir[260];
  char srcname[80];
  char dstdir[260];
  char dstname[80];
  uint8 src_dhandle = 0;
  uint8 dst_dhandle = 0;
  int src_allocated = 0;
  int dst_allocated = 0;
  NCP_NDIR_INFO sinfo;
  uint32 attrs;
  int rc;

  memset(&sinfo, 0, sizeof(sinfo));

  if (ncopy_split_dir_name(src, srcdir, srcname) ||
      ncopy_split_dir_name(dst, dstdir, dstname))
    return;

  if (ncopy_alloc_parent_handle(src, &src_dhandle, &src_allocated))
    return;

  if (ncopy_alloc_parent_handle(dst, &dst_dhandle, &dst_allocated)) {
    if (src_allocated)
      dealloc_dir_handle(src_dhandle);
    return;
  }

  attrs = (uint32)(fallback_attr & (_A_RDONLY | _A_HIDDEN |
                                    _A_SYSTEM | _A_ARCH));
  rc = ncp87_06_obtain_ndir_info(srcname, src_dhandle,
                                  &sinfo, NULL, NULL, NULL);
  if (!rc)
    attrs = sinfo.attributes;

  rc = ncp87_07_modify_dos_attributes(dstname, dst_dhandle, attrs,
                                       NULL, NULL, NULL);
  ncopy_debug_rc("restore NetWare attrs NCP87/07", rc);
  ncopy_debug_ul("restore NetWare attrs", attrs);

  if (dst_allocated)
    dealloc_dir_handle(dst_dhandle);
  if (src_allocated)
    dealloc_dir_handle(src_dhandle);
}

static int ncopy_copy_one(char *src, char *dst, struct find_t *ff, int options)
{
  unsigned attr;
  int d;
  char srcdir[260];
  char srcname[80];
  char dstdir[260];
  char dstname[80];

  if (ncopy_same_path(src, dst)) {
    fprintf(stdout, " The file will NOT be copied because the source and destination file names are the same.\n");
    return(-1);
  }

  if ((options & (NCOPY_OPT_A | NCOPY_OPT_M)) && !(ff->attrib & _A_ARCH))
    return(1);

  if (ncopy_split_dir_name(src, srcdir, srcname)) {
    strmaxcpy(srcdir, ".", sizeof(srcdir) - 1);
    ncopy_basename(srcname, src, sizeof(srcname));
  }
  if (ncopy_split_dir_name(dst, dstdir, dstname)) {
    strmaxcpy(dstdir, ".", sizeof(dstdir) - 1);
    ncopy_basename(dstname, dst, sizeof(dstname));
  }

  ncopy_print_group(srcdir, dstdir);
  ncopy_print_file_map(srcname, dstname);

  if (ncopy_copy_data(src, dst, (unsigned long)ff->size))
    return(-1);

  d = open(dst, O_WRONLY | O_BINARY);
  if (d >= 0) {
    _dos_setftime(d, ff->wr_date, ff->wr_time);
    close(d);
  } else {
    fprintf(stdout, " Failure setting destination file date and time.\n");
  }

  attr = ff->attrib & (_A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH);
  if (_dos_setfileattr(dst, attr))
    fprintf(stdout, " Error setting file information.\n");

  /* Preserve high NetWare DOS attributes such as Delete/Rename Inhibit.
   * DOS setfileattr can only express the low DOS bits, which was the reason
   * B.TXT ended up as [Ro-A--------------] instead of carrying DI/RI. */
  ncopy_restore_netware_attrs(src, dst, attr);

  if ((options & NCOPY_OPT_V) && ncopy_verify_file(src, dst))
    fprintf(stdout, " Error copying file.\n");

  if (options & NCOPY_OPT_M) {
    unsigned newattr = ff->attrib & ~_A_ARCH;
    _dos_setfileattr(src, newattr & (_A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH));
  }

  return(0);
}

static int ncopy_copy_pattern(char *srcdir, char *pattern, char *dstdir,
                              int options, int *copied, int *failed);

static int ncopy_copy_subdirs(char *srcdir, char *dstdir, int options,
                              int *copied, int *failed)
{
  struct find_t ff;
  char search[260];
  int rc;

  ncopy_join(search, srcdir, "*.*", sizeof(search));
  rc = _dos_findfirst(search, _A_NORMAL | _A_RDONLY | _A_HIDDEN |
                              _A_SYSTEM | _A_ARCH | _A_SUBDIR, &ff);
  while (rc == 0) {
    if ((ff.attrib & _A_SUBDIR) && !tool_is_dot_dir(ff.name)) {
      char childsrc[260];
      char childdst[260];
      int before;

      ncopy_join(childsrc, srcdir, ff.name, sizeof(childsrc));
      ncopy_join(childdst, dstdir, ff.name, sizeof(childdst));

      before = *copied;
      ncopy_print_group(childsrc, childdst);
      if (options & NCOPY_OPT_E)
        ncopy_make_dir(childdst);

      ncopy_copy_pattern(childsrc, "*.*", childdst, options, copied, failed);
      ncopy_copy_subdirs(childsrc, childdst, options, copied, failed);

      if (*copied > before)
        ncopy_make_dir(childdst);
    }
    rc = _dos_findnext(&ff);
  }
  return(0);
}

static int ncopy_copy_pattern(char *srcdir, char *pattern, char *dstdir,
                              int options, int *copied, int *failed)
{
  struct find_t ff;
  char search[260];
  int rc;
  int any = 0;

  ncopy_join(search, srcdir, pattern, sizeof(search));
  rc = _dos_findfirst(search, _A_NORMAL | _A_RDONLY | _A_HIDDEN |
                              _A_SYSTEM | _A_ARCH, &ff);
  while (rc == 0) {
    if (!(ff.attrib & _A_SUBDIR)) {
      char src[260];
      char dst[260];
      int r;
      any = 1;
      ncopy_join(src, srcdir, ff.name, sizeof(src));
      ncopy_join(dst, dstdir, ff.name, sizeof(dst));
      if (ncopy_make_dir(dstdir)) {
        fprintf(stdout, " Illegal destination.\n");
        return(-1);
      }
      r = ncopy_copy_one(src, dst, &ff, options);
      if (r == 0)
        (*copied)++;
      else if (r < 0)
        (*failed)++;
    }
    rc = _dos_findnext(&ff);
  }

  if ((options & NCOPY_OPT_S) != 0)
    ncopy_copy_subdirs(srcdir, dstdir, options, copied, failed);

  return(any ? 0 : 1);
}

static int ncopy_copy_single(char *src, char *dstarg, int options,
                             int *copied, int *failed)
{
  struct find_t ff;
  char dst[260];
  char base[80];
  int rc;

  rc = _dos_findfirst(src, _A_NORMAL | _A_RDONLY | _A_HIDDEN |
                           _A_SYSTEM | _A_ARCH, &ff);
  if (rc)
    return(1);
  if (ff.attrib & _A_SUBDIR)
    return(1);

  if (ncopy_is_dir(dstarg)) {
    ncopy_basename(base, src, sizeof(base));
    ncopy_join(dst, dstarg, base, sizeof(dst));
  } else {
    strmaxcpy(dst, dstarg, sizeof(dst) - 1);
  }

  rc = ncopy_copy_one(src, dst, &ff, options);
  if (rc == 0)
    (*copied)++;
  else if (rc < 0)
    (*failed)++;
  return(0);
}

static void ncopy_summary(int copied, int failed)
{
  if (copied == 0)
    fprintf(stdout, "     No files copied.\n");
  else
    fprintf(stdout, "     %d file%s copied.\n", copied, copied == 1 ? "" : "s");
  if (failed)
    fprintf(stdout, "     %d file%s NOT copied.\n", failed, failed == 1 ? "" : "s");
}

static int ncopy_upper_char(int c)
{
  if (c >= 'a' && c <= 'z')
    return(c - ('a' - 'A'));
  return(c);
}

static int ncopy_parse_option(char *s, int *options)
{
  char *p;
  int c1;
  int c2;
  int c3;

  if (!s || !*s)
    return(0);

  /* Novell NCOPY accepts case-insensitive options anywhere on the
   * command line, including after the destination path:
   *   NCOPY SRC DST /V
   *   NCOPY SRC TO DST /S /E
   *   NCOPY /A SRC TO DST /S
   * Keep this parser independent of shared helpers so DOS/Watcom argv
   * strings such as lowercase /v are handled exactly here. */
  if (s[0] != '/' && s[0] != '-')
    return(0);

  p = s;
  while (*p == '/' || *p == '-')
    p++;

  if (!*p)
    return(-1);

  c1 = ncopy_upper_char((unsigned char)p[0]);
  c2 = ncopy_upper_char((unsigned char)p[1]);
  c3 = ncopy_upper_char((unsigned char)p[2]);

  if (c1 == '?' && p[1] == '\0')
    return(2);

  if (c1 == 'H' && p[1] == '\0')
    return(2);

  if (c1 == 'H' && c2 == 'E' && c3 == 'L' &&
      ncopy_upper_char((unsigned char)p[3]) == 'P' && p[4] == '\0')
    return(2);

  if (c1 == 'S' && p[1] == '\0') { *options |= NCOPY_OPT_S; return(1); }
  if (c1 == 'E' && p[1] == '\0') { *options |= NCOPY_OPT_E; return(1); }
  if (c1 == 'V' && p[1] == '\0') { *options |= NCOPY_OPT_V; return(1); }
  if (c1 == 'A' && p[1] == '\0') { *options |= NCOPY_OPT_A; return(1); }
  if (c1 == 'M' && p[1] == '\0') { *options |= NCOPY_OPT_M | NCOPY_OPT_A; return(1); }
  if (c1 == 'I' && p[1] == '\0') { *options |= NCOPY_OPT_I; return(1); }
  if (c1 == 'C' && p[1] == '\0') { *options |= NCOPY_OPT_C; return(1); }
  if (c1 == 'F' && p[1] == '\0') { *options |= NCOPY_OPT_F; return(1); }

  if (c1 == 'S' && c2 == '/' && c3 == 'E' && p[3] == '\0') {
    *options |= NCOPY_OPT_S | NCOPY_OPT_E;
    return(1);
  }

  if (c1 == 'S' && c2 == 'E' && p[2] == '\0') {
    *options |= NCOPY_OPT_S | NCOPY_OPT_E;
    return(1);
  }

  return(-1);
}

static int ncopy_is_to_separator(char *s)
{
  return(s && tool_strsame(s, "TO"));
}

int func_ncopy(int argc, char *argv[], int mode)
{
  char *src = NULL;
  char *dst = NULL;
  int options = 0;
  int i;
  int copied = 0;
  int failed = 0;
  int saw_to = 0;
  int result;
  char srcdir[260];
  char pat[260];

  (void)mode;

  if (ncopy_debug_enabled()) {
    fprintf(stdout, "NCOPYDBG: func_ncopy entered argc=%d\n", argc);
    for (i = 0; i < argc; i++)
      fprintf(stdout, "NCOPYDBG: argv[%d]=<%s>\n",
              i, argv[i] ? argv[i] : "");
  }

  if (argc <= 1) {
    ncopy_usage();
    return(1);
  }

  for (i = 1; i < argc; i++) {
    int opt = ncopy_parse_option(argv[i], &options);

    /* Options are legal before the source, between source and TO, and after
     * the destination.  Parse them before treating anything as a path. */
    if (opt == 2) {
      ncopy_usage();
      return(0);
    }
    if (opt == 1)
      continue;
    if (opt < 0) {
      ncopy_usage();
      return(1);
    }

    /* TO is an optional, case-insensitive separator, not a required keyword.
     * NCOPY SRC DST and NCOPY SRC TO DST are equivalent. */
    if (ncopy_is_to_separator(argv[i])) {
      saw_to = 1;
      continue;
    }

    if (!src)
      src = argv[i];
    else if (!dst)
      dst = argv[i];
    else {
      fprintf(stdout, "You cannot copy multiple files to a single file.\n");
      return(1);
    }
  }

  if (saw_to && !dst) {
    ncopy_usage();
    return(1);
  }

  if ((options & NCOPY_OPT_E) && !(options & NCOPY_OPT_S)) {
    fprintf(stdout, "The /E parameter (copy empty directories) is only valid with the /S parameter (copy sub directories).\n");
    return(1);
  }

  if (!src || !dst) {
    ncopy_usage();
    return(1);
  }

  ncopy_reset_output_group();

  if (tool_has_wildcards(src) || (options & NCOPY_OPT_S)) {
    if (!ncopy_is_dir(dst)) {
      fprintf(stdout, " Illegal destination.\n");
      ncopy_summary(0, 0);
      return(1);
    }
    if ((options & NCOPY_OPT_S) && ncopy_is_dir(src) && !tool_has_wildcards(src)) {
      /* Novell NCOPY accepts a directory as the source root with /S. */
      strmaxcpy(srcdir, src, sizeof(srcdir) - 1);
      strmaxcpy(pat, "*.*", sizeof(pat) - 1);
    } else {
      tool_parent_pattern(srcdir, pat, src, 260, 260);
      if (!srcdir[0])
        strmaxcpy(srcdir, ".", sizeof(srcdir) - 1);
      if (!pat[0])
        strmaxcpy(pat, "*.*", sizeof(pat) - 1);
    }
    result = ncopy_copy_pattern(srcdir, pat, dst, options, &copied, &failed);
  } else {
    result = ncopy_copy_single(src, dst, options, &copied, &failed);
  }

  if (result > 0) {
    fprintf(stdout, " Files not found.\n");
    ncopy_summary(0, failed);
    return(1);
  }

  ncopy_finish_output_groups();
  ncopy_summary(copied, failed);
  return(failed ? 1 : 0);
}
