/*
 * mars-nwe-dosutils - NetWare/DOS utility tools.
 *
 * Copyright (C) 2026 Mario Fetka
 * Copyright (C) 1993,1996 Martin Stover, Marburg, Germany
 *
 * 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: NWTESTS diagnostic utility for exercising NetWare requester, NCP and namespace behavior.
 * Depends on: net.h, ncpapi.h, netcall.c requester helpers, ncpapi.c namespace/NCP helpers, tools.c shared utility routines.
 */

#include "net.h"
#include "ncpapi.h"

#define TEST_RIGHT_S 0x01
#define TEST_RIGHT_R 0x02
#define TEST_RIGHT_W 0x04
#define TEST_RIGHT_C 0x08
#define TEST_RIGHT_E 0x10
#define TEST_RIGHT_M 0x20
#define TEST_RIGHT_F 0x40
#define TEST_RIGHT_A 0x80

#define TEST_NCP_RIGHT_READ   0x0001
#define TEST_NCP_RIGHT_WRITE  0x0002
#define TEST_NCP_RIGHT_CREATE 0x0008
#define TEST_NCP_RIGHT_DELETE 0x0010
#define TEST_NCP_RIGHT_OWNER  0x0020
#define TEST_NCP_RIGHT_SEARCH 0x0040
#define TEST_NCP_RIGHT_MODIFY 0x0080
#define TEST_NCP_RIGHT_SUPER  0x0100

static void tests_usage(void)
{
  fprintf(stdout, "Usage: TESTS [NCP87C32ATTR|NCP87C32AUTO|EFFRIGHT path|NCP22S4 path [mask]|NCP23MAP path|NCP22REN old new|RENDIR parent]\n");
  fprintf(stdout, "       TESTS NCP2225MISS [missing-file]\n");
  fprintf(stdout, "       TESTS NCP2225ATTR file [attr]   (current-dir 8.3 file; attr hex)\n");
  fprintf(stdout, "       TESTS NCP2225TIME file [date time] (DOS hex words; modifies mtime)\n");
  fprintf(stdout, "       TESTS NCP2225ADATE file [date]    (DOS hex word; modifies access date)\n");
  fprintf(stdout, "       TESTS NCP2225ARCH file [date time id] (archive xattr metadata)\n");
  fprintf(stdout, "       TESTS NCP2225CREATE file [date time id] (create xattr metadata)\n");
  fprintf(stdout, "       TESTS NCP2225MODID file [id] (modifier-id xattr metadata)\n");
  fprintf(stdout, "       TESTS NCP2225MAXSPACE dir [blocks] (directory maximum-space/quota)\n");
  fprintf(stdout, "       TESTS NCP221EINFO dir      (dump NCP22/1E directory info layout)\n");
  fprintf(stdout, "       TESTS NCP22S4 path RF|42|0x42|ALL|NONE\n");
  fprintf(stdout, "       TESTS NCP22REN oldpath newpath   (NCP22/2E Rename Or Move old)\n");
  fprintf(stdout, "       TESTS RENDIR parent              (rename parent\\TREN <-> parent\\TREN2)\n");
  fprintf(stdout, "       TESTS NCP23MAP path   (probe NCP23/F4 then F3 layouts; see server debug log)\n");
}

static uint8 tests_map_ncp_mask(uint16 ncp_rights)
{
  uint8 mask = 0;

  if (ncp_rights & TEST_NCP_RIGHT_SUPER)  mask |= TEST_RIGHT_S;
  if (ncp_rights & TEST_NCP_RIGHT_READ)   mask |= TEST_RIGHT_R;
  if (ncp_rights & TEST_NCP_RIGHT_WRITE)  mask |= TEST_RIGHT_W;
  if (ncp_rights & TEST_NCP_RIGHT_CREATE) mask |= TEST_RIGHT_C;
  if (ncp_rights & TEST_NCP_RIGHT_DELETE) mask |= TEST_RIGHT_E;
  if (ncp_rights & TEST_NCP_RIGHT_MODIFY) mask |= TEST_RIGHT_M;
  if (ncp_rights & TEST_NCP_RIGHT_SEARCH) mask |= TEST_RIGHT_F;
  if (ncp_rights & TEST_NCP_RIGHT_OWNER)  mask |= TEST_RIGHT_A;

  return(mask);
}

static void tests_mask_string(uint8 mask, char *out)
{
  out[0] = (mask & TEST_RIGHT_S) ? 'S' : '-';
  out[1] = (mask & TEST_RIGHT_R) ? 'R' : '-';
  out[2] = (mask & TEST_RIGHT_W) ? 'W' : '-';
  out[3] = (mask & TEST_RIGHT_C) ? 'C' : '-';
  out[4] = (mask & TEST_RIGHT_E) ? 'E' : '-';
  out[5] = (mask & TEST_RIGHT_M) ? 'M' : '-';
  out[6] = (mask & TEST_RIGHT_F) ? 'F' : '-';
  out[7] = (mask & TEST_RIGHT_A) ? 'A' : '-';
  out[8] = '\0';
}


static void tests_print_mask(char *label, int rc, uint8 mask)
{
  char s[10];

  if (rc) {
    fprintf(stdout, "%-18.18s rc=%d\n", label, rc);
    return;
  }

  tests_mask_string(mask, s);
  fprintf(stdout, "%-18.18s [%s] %02X\n", label, s, mask);
}

static void tests_print_eff_header(char *title)
{
  if (title && *title)
    fprintf(stdout, "\n%s\n", title);
  fprintf(stdout, "%-18s %5s %-10s %s\n", "call", "rc", "mask", "raw");
  fprintf(stdout, "%-18s %5s %-10s %s\n", "------------------", "-----", "----------", "----");
}

static void tests_print_eff_row(char *label, int rc, uint8 mask,
                                uint16 raw, int has_raw)
{
  char s[10];

  if (rc) {
    fprintf(stdout, "%-18.18s %5d %-10s %s\n", label, rc, "-", "-");
    return;
  }

  tests_mask_string(mask, s);
  if (has_raw)
    fprintf(stdout, "%-18.18s %5d [%s] %04X\n", label, 0, s, raw);
  else
    fprintf(stdout, "%-18.18s %5d [%s] %s\n", label, 0, s, "-");
}

static int tests_hex_value(int ch)
{
  if (ch >= '0' && ch <= '9') return(ch - '0');
  if (ch >= 'a' && ch <= 'f') return(ch - 'a' + 10);
  if (ch >= 'A' && ch <= 'F') return(ch - 'A' + 10);
  return(-1);
}

static int tests_parse_irm_mask(char *s, uint8 *mask_out)
{
  uint16 mask = 0;
  int i;
  int hex = 0;

  if (!s || !*s || !mask_out)
    return(-1);

  if (tool_strsame(s, "ALL")) {
    *mask_out = 0xff;
    return(0);
  }

  if (tool_strsame(s, "NONE") || tool_strsame(s, "N")) {
    *mask_out = 0x00;
    return(0);
  }

  if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
    if (!s[2])
      return(-1);
    for (i = 2; s[i]; i++) {
      int v = tests_hex_value(s[i]);
      if (v < 0)
        return(-1);
      mask = (uint16)((mask << 4) | v);
      if (mask > 0xff)
        return(-1);
    }
    *mask_out = (uint8)mask;
    return(0);
  }

  /*
   * Treat a plain one/two digit value as hexadecimal.  That makes the DOS
   * command natural for NetWare rights masks: "42" means R+F, not decimal 42.
   */
  if (strlen(s) <= 2) {
    hex = 1;
    for (i = 0; s[i]; i++) {
      if (s[i] < '0' || s[i] > '9') {
        hex = 0;
        break;
      }
    }
    if (hex) {
      for (i = 0; s[i]; i++)
        mask = (uint16)((mask << 4) | tests_hex_value(s[i]));
      *mask_out = (uint8)mask;
      return(0);
    }
  }

  for (i = 0; s[i]; i++) {
    int ch = s[i];

    if (ch >= 'a' && ch <= 'z') ch -= 32;
    if (ch == ' ' || ch == ',' || ch == '+' || ch == '-' || ch == '/')
      continue;

    switch(ch) {
      case 'S': mask |= TEST_RIGHT_S; break;
      case 'R': mask |= TEST_RIGHT_R; break;
      case 'W': mask |= TEST_RIGHT_W; break;
      case 'C': mask |= TEST_RIGHT_C; break;
      case 'E':
      case 'D': mask |= TEST_RIGHT_E; break;
      case 'M': mask |= TEST_RIGHT_M; break;
      case 'F': mask |= TEST_RIGHT_F; break;
      case 'A': mask |= TEST_RIGHT_A; break;
      default: return(-1);
    }
  }

  *mask_out = (uint8)mask;
  return(0);
}


static int tests_parse_byte_arg(char *s, uint8 *value_out)
{
  uint16 value = 0;
  int i;

  if (!s || !*s || !value_out)
    return(-1);

  if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
    if (!s[2])
      return(-1);
    for (i = 2; s[i]; i++) {
      int v = tests_hex_value(s[i]);
      if (v < 0)
        return(-1);
      value = (uint16)((value << 4) | v);
      if (value > 0xff)
        return(-1);
    }
    *value_out = (uint8)value;
    return(0);
  }

  /* Plain values are hexadecimal, matching the existing rights-mask parser. */
  for (i = 0; s[i]; i++) {
    int v = tests_hex_value(s[i]);
    if (v < 0)
      return(-1);
    value = (uint16)((value << 4) | v);
    if (value > 0xff)
      return(-1);
  }

  *value_out = (uint8)value;
  return(0);
}


static int tests_parse_word_arg(char *s, uint16 *value_out)
{
  uint32 value = 0;
  int i;

  if (!s || !*s || !value_out)
    return(-1);

  if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
    if (!s[2])
      return(-1);
    s += 2;
  }

  for (i = 0; s[i]; i++) {
    int v = tests_hex_value(s[i]);
    if (v < 0)
      return(-1);
    value = (value << 4) | (uint32)v;
    if (value > 0xffffUL)
      return(-1);
  }

  *value_out = (uint16)value;
  return(0);
}

#define TEST_DM_ATTRIBUTES   0x00000002UL
#define TEST_DM_CREATE_DATE  0x00000004UL
#define TEST_DM_CREATE_TIME  0x00000008UL
#define TEST_DM_CREATOR_ID   0x00000010UL
#define TEST_DM_ARCHIVE_DATE 0x00000020UL
#define TEST_DM_ARCHIVE_TIME 0x00000040UL
#define TEST_DM_ARCHIVER_ID  0x00000080UL
#define TEST_DM_MODIFY_DATE  0x00000100UL
#define TEST_DM_MODIFY_TIME 0x00000200UL
#define TEST_DM_MODIFIER_ID 0x00000400UL
#define TEST_DM_LAST_ACCESS_DATE 0x00000800UL
#define TEST_DM_MAXIMUM_SPACE 0x00002000UL

static int tests_parse_dword_arg(char *s, uint32 *value_out)
{
  uint32 value = 0;
  int i;

  if (!s || !*s || !value_out)
    return(-1);

  if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
    if (!s[2])
      return(-1);
    s += 2;
  }

  for (i = 0; s[i]; i++) {
    int v = tests_hex_value(s[i]);
    if (v < 0)
      return(-1);
    value = (value << 4) | (uint32)v;
  }

  *value_out = value;
  return(0);
}


static int tests_read_attr_by_ncp87(uint8 dhandle, char *path, uint8 *attr_out)
{
  uint32 attr = 0;
  int rc;

  if (attr_out)
    *attr_out = 0;

  rc = ncp87_06_obtain_rim_attributes(tool_is_current_path(path) ? "" : path,
                                       (uint16)dhandle,
                                       &attr,
                                       NULL, NULL, NULL);
  if (rc)
    return(rc);

  if (attr_out)
    *attr_out = (uint8)(attr & 0xffUL);
  return(0);
}

static int tests_ncp22_25_set_attr(uint8 dhandle, char *path, uint8 attr)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  rest[104];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x06;
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_ATTRIBUTES, req.change_bits);
  U32_TO_32((uint32)attr, req.attributes);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}

static int tests_ncp2225miss(char *path)
{
  uint8 dhandle = 0;
  int rc;

  if (!path || !*path)
    path = "NO22_25.$$$";

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225MISS failed: current drive is not a network drive\n");
    return(1);
  }

  rc = tests_ncp22_25_set_attr(dhandle, path, 0x20);
  fprintf(stdout, "NCP2225MISS %s neterrno=0x%02X\n", path, (unsigned)(-rc & 0xff));
  if (rc != -0xff) {
    fprintf(stdout, "NCP2225MISS failed: expected completion 0xFF for missing entry\n");
    return(1);
  }
  return(0);
}

static int tests_ncp2225attr(char *path, char *attr_arg)
{
  uint8 dhandle = 0;
  uint8 before = 0;
  uint8 target = 0;
  uint8 after = 0;
  uint8 restored = 0;
  int rc;
  int restore = 0;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225ATTR file [attr]   (file must be current-dir 8.3 name)\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225ATTR failed: current drive is not a network drive\n");
    return(1);
  }

  rc = tests_read_attr_by_ncp87(dhandle, path, &before);
  if (rc) {
    fprintf(stdout, "NCP2225ATTR %s read-before rc=%d\n", path, rc);
    return(rc);
  }

  if (attr_arg) {
    if (tests_parse_byte_arg(attr_arg, &target)) {
      fprintf(stdout, "Bad attr '%s'. Use e.g. 20, 21, 0x21.\n", attr_arg);
      return(1);
    }
  } else {
    target = (uint8)(before ^ 0x01);  /* default: toggle read-only bit */
    restore = 1;
  }

  fprintf(stdout, "NCP2225ATTR %s before=0x%02X target=0x%02X\n",
          path, (unsigned)before, (unsigned)target);

  rc = tests_ncp22_25_set_attr(dhandle, path, target);
  fprintf(stdout, "ncp22/25 set attr rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  rc = tests_read_attr_by_ncp87(dhandle, path, &after);
  if (rc) {
    fprintf(stdout, "NCP2225ATTR %s read-after rc=%d\n", path, rc);
    return(rc);
  }
  fprintf(stdout, "NCP2225ATTR %s after=0x%02X\n", path, (unsigned)after);

  if (after != target) {
    fprintf(stdout, "NCP2225ATTR failed: expected attr=0x%02X\n", (unsigned)target);
    return(1);
  }

  if (restore) {
    rc = tests_ncp22_25_set_attr(dhandle, path, before);
    fprintf(stdout, "ncp22/25 restore attr rc=%d neterrno=0x%02X\n",
            rc, (unsigned)(rc < 0 ? -rc : 0));
    if (rc)
      return(rc);

    rc = tests_read_attr_by_ncp87(dhandle, path, &restored);
    if (rc) {
      fprintf(stdout, "NCP2225ATTR %s read-restore rc=%d\n", path, rc);
      return(rc);
    }
    fprintf(stdout, "NCP2225ATTR %s restored=0x%02X\n", path, (unsigned)restored);
    if (restored != before) {
      fprintf(stdout, "NCP2225ATTR failed: expected restored attr=0x%02X\n",
              (unsigned)before);
      return(1);
    }
  }

  return(0);
}

static int tests_ncp22_25_set_mtime(uint8 dhandle, char *path, uint16 dos_date, uint16 dos_time)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  updated_time[2];
    uint8  updated_date[2];
    uint8  updated_id[4];
    uint8  rest[80];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x06;
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_MODIFY_DATE | TEST_DM_MODIFY_TIME, req.change_bits);
  U16_TO_16(dos_time, req.updated_time);
  U16_TO_16(dos_date, req.updated_date);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}



static int tests_ncp22_25_set_archive(uint8 dhandle, char *path,
                                      uint16 dos_date, uint16 dos_time,
                                      uint32 archiver_id)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  rest[88];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x06;
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_ARCHIVE_DATE | TEST_DM_ARCHIVE_TIME | TEST_DM_ARCHIVER_ID,
            req.change_bits);
  U16_TO_16(dos_time, req.archived_time);
  U16_TO_16(dos_date, req.archived_date);
  U32_TO_BE32(archiver_id, req.archived_id);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}




static int tests_ncp22_25_set_modifier(uint8 dhandle, char *path, uint32 modifier_id)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  updated_time[2];
    uint8  updated_date[2];
    uint8  updated_id[4];
    uint8  rest[80];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x06;
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_MODIFIER_ID, req.change_bits);
  U32_TO_BE32(modifier_id, req.updated_id);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}

static int tests_ncp22_25_set_create(uint8 dhandle, char *path,
                                     uint16 dos_date, uint16 dos_time,
                                     uint32 creator_id)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  rest[96];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x06;
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_CREATE_DATE | TEST_DM_CREATE_TIME | TEST_DM_CREATOR_ID,
            req.change_bits);
  U16_TO_16(dos_time, req.created_time);
  U16_TO_16(dos_date, req.created_date);
  U32_TO_BE32(creator_id, req.created_id);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}

static int tests_ncp22_25_set_adate(uint8 dhandle, char *path, uint16 dos_date)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  updated_time[2];
    uint8  updated_date[2];
    uint8  updated_id[4];
    uint8  size[4];
    uint8  reserved_1[44];
    uint8  inherited_rights_mask[2];
    uint8  last_access_date[2];
    uint8  rest[28];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x06;
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_LAST_ACCESS_DATE, req.change_bits);
  U16_TO_16(dos_date, req.last_access_date);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}


static int tests_ncp22_25_set_maxspace(uint8 dhandle, char *path, uint32 max_space)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  change_bits[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  modify_time[2];
    uint8  modify_date[2];
    uint8  next_trustee[4];
    uint8  reserved_1[48];
    uint8  max_space[4];
    uint8  inherited_rights_mask[2];
    uint8  rest[26];
  } req;
  struct {
    uint16 len;
  } repl = { 0 };

  memset(&req, 0, sizeof(req));
  req.func = 0x25;
  req.dirhandle = dhandle;
  req.search_attributes = 0x10;  /* directory */
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  U32_TO_32(TEST_DM_MAXIMUM_SPACE, req.change_bits);
  U32_TO_32(0x10UL, req.attributes);
  U32_TO_BE32(max_space, req.max_space);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);
  return(0);
}


static void tests_print_dos_datetime(char *label, uint16 date, uint16 timev);

static uint16 tests_get_be16(uint8 *p)
{
  return((uint16)(((uint16)p[0] << 8) | p[1]));
}

static uint16 tests_get_le16(uint8 *p)
{
  return((uint16)(((uint16)p[1] << 8) | p[0]));
}

static uint32 tests_get_le32(uint8 *p)
{
  return(((uint32)p[3] << 24) | ((uint32)p[2] << 16) |
         ((uint32)p[1] << 8) | (uint32)p[0]);
}

static int tests_ncp22_1e_read_dir_maxspace(uint8 dhandle, char *path, uint32 *max_space_out)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  namlen;
    uint8  name[12];
  } req;
  struct {
    uint16 len;
    uint8  searchsequence[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  modify_time[2];
    uint8  modify_date[2];
    uint8  next_trustee[4];
    uint8  reserved_1[48];
    uint8  max_space[4];
    uint8  inherited_rights_mask[2];
    uint8  rest[26];
  } repl;

  if (max_space_out)
    *max_space_out = 0;

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  /*
   * Net_Call expects repl.len to contain the available reply payload size.
   * Leaving it zero makes DOS report rc=-51 even when mars_nwe returns a
   * valid NCP22/1E directory entry.
   */
  repl.len = sizeof(repl) - sizeof(repl.len);
  req.func = 0x1e;
  req.dirhandle = dhandle;
  req.search_attributes = 0x10;  /* directory */
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);

  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno)
    return(-neterrno);

  if (max_space_out)
    *max_space_out = GET_BE32(repl.max_space);
  return(0);
}



static int tests_ncp221einfo(char *path)
{
  uint8 dhandle = 0;
  char outname[14];
  uint16 ctime_be, cdate_be;
  uint16 atime_be, adate_be;
  uint16 mtime_be, mdate_be;
  uint16 ctime_le, cdate_le;
  uint16 atime_le, adate_le;
  uint16 mtime_le, mdate_le;
  uint16 irm_be, irm_le;
  uint32 attr_be, attr_le;
  uint32 max_be, max_le;
  uint32 cid_be, aid_be;
  uint32 subdir_be;
  int rc;
  struct {
    uint16 len;
    uint8  func;
    uint8  dirhandle;
    uint8  search_attributes;
    uint8  searchsequence[4];
    uint8  namlen;
    uint8  name[12];
  } req;
  struct {
    uint16 len;
    uint8  searchsequence[4];
    uint8  subdir[4];
    uint8  attributes[4];
    uint8  uniqueid;
    uint8  flags;
    uint8  namespace;
    uint8  namlen;
    uint8  name[12];
    uint8  created_time[2];
    uint8  created_date[2];
    uint8  created_id[4];
    uint8  archived_time[2];
    uint8  archived_date[2];
    uint8  archived_id[4];
    uint8  modify_time[2];
    uint8  modify_date[2];
    uint8  next_trustee[4];
    uint8  reserved_1[48];
    uint8  max_space[4];
    uint8  inherited_rights_mask[2];
    uint8  rest[26];
  } repl;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP221EINFO dir\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP221EINFO failed: current drive is not a network drive\n");
    return(1);
  }

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  repl.len = sizeof(repl) - sizeof(repl.len);
  req.func = 0x1e;
  req.dirhandle = dhandle;
  req.search_attributes = 0x10;  /* directory */
  U32_TO_BE32(0xffffffffUL, req.searchsequence);
  if (tool_copy_ncp22_name(req.name, path, &req.namlen))
    return(-1);
  req.len = sizeof(req) - sizeof(req.len);

  neterrno = Net_Call(0xE200, &req, &repl);
  if (neterrno) {
    rc = -neterrno;
    fprintf(stdout, "NCP221EINFO %s rc=%d neterrno=0x%02X\n",
            path, rc, (unsigned)neterrno);
    return(rc);
  }

  memset(outname, 0, sizeof(outname));
  if (repl.namlen > 12)
    repl.namlen = 12;
  memcpy(outname, repl.name, repl.namlen);

  attr_be = GET_BE32(repl.attributes);
  attr_le = tests_get_le32(repl.attributes);
  subdir_be = GET_BE32(repl.subdir);
  max_be = GET_BE32(repl.max_space);
  max_le = tests_get_le32(repl.max_space);
  irm_be = tests_get_be16(repl.inherited_rights_mask);
  irm_le = tests_get_le16(repl.inherited_rights_mask);

  ctime_be = tests_get_be16(repl.created_time);
  cdate_be = tests_get_be16(repl.created_date);
  atime_be = tests_get_be16(repl.archived_time);
  adate_be = tests_get_be16(repl.archived_date);
  mtime_be = tests_get_be16(repl.modify_time);
  mdate_be = tests_get_be16(repl.modify_date);

  ctime_le = tests_get_le16(repl.created_time);
  cdate_le = tests_get_le16(repl.created_date);
  atime_le = tests_get_le16(repl.archived_time);
  adate_le = tests_get_le16(repl.archived_date);
  mtime_le = tests_get_le16(repl.modify_time);
  mdate_le = tests_get_le16(repl.modify_date);

  cid_be = GET_BE32(repl.created_id);
  aid_be = GET_BE32(repl.archived_id);

  fprintf(stdout, "NCP221EINFO %s OK name=%s len=%u repl_len=%u\n",
          path, outname, (unsigned)repl.namlen, (unsigned)repl.len);
  fprintf(stdout, "NCP221EINFO seq=0x%08lX subdir=0x%08lX unique=0x%02X flags=0x%02X ns=0x%02X\n",
          (unsigned long)GET_BE32(repl.searchsequence),
          (unsigned long)subdir_be,
          (unsigned)repl.uniqueid, (unsigned)repl.flags, (unsigned)repl.namespace);
  fprintf(stdout, "NCP221EINFO attr=0x%08lX (raw be=0x%08lX) max=0x%08lX (raw le=0x%08lX) irm=0x%04X (raw be=0x%04X)\n",
          (unsigned long)attr_le, (unsigned long)attr_be,
          (unsigned long)max_be, (unsigned long)max_le,
          (unsigned)irm_le, (unsigned)irm_be);
  fprintf(stdout, "NCP221EINFO ids create=0x%08lX archive=0x%08lX nextTrustee=0x%08lX\n",
          (unsigned long)cid_be, (unsigned long)aid_be,
          (unsigned long)GET_BE32(repl.next_trustee));

  /*
   * NCP22/1E uses mixed byte order in this old DOS layout:
   * attributes, DOS date/time and IRM are little-endian;
   * max_space and object IDs are big-endian in the mars_nwe reply.
   */
  tests_print_dos_datetime("NCP221EINFO create", cdate_le, ctime_le);
  tests_print_dos_datetime("NCP221EINFO archive", adate_le, atime_le);
  tests_print_dos_datetime("NCP221EINFO modify ", mdate_le, mtime_le);
  return(0);
}

static void tests_print_dos_datetime(char *label, uint16 date, uint16 timev)
{
  unsigned year = ((date >> 9) & 0x7f) + 1980;
  unsigned month = (date >> 5) & 0x0f;
  unsigned day = date & 0x1f;
  unsigned hour = (timev >> 11) & 0x1f;
  unsigned minute = (timev >> 5) & 0x3f;
  unsigned second = (timev & 0x1f) * 2;

  fprintf(stdout, "%s %04u-%02u-%02u %02u:%02u:%02u date=0x%04X time=0x%04X\n",
          label, year, month, day, hour, minute, second,
          (unsigned)date, (unsigned)timev);
}


static void tests_print_dos_date(char *label, uint16 date)
{
  unsigned year = ((date >> 9) & 0x7f) + 1980;
  unsigned month = (date >> 5) & 0x0f;
  unsigned day = date & 0x1f;

  fprintf(stdout, "%s %04u-%02u-%02u date=0x%04X\n",
          label, year, month, day, (unsigned)date);
}


static int tests_ncp2225time(char *path, char *date_arg, char *time_arg)
{
  uint8 dhandle = 0;
  NCP_NDIR_INFO before;
  NCP_NDIR_INFO after;
  NCP_NDIR_INFO restored;
  uint16 target_date = 0x5c99;  /* 2026-05-25 */
  uint16 target_time = 0xa320;  /* 20:25:00 */
  int rc;
  int restore = 1;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225TIME file [datehex timehex]\n");
    return(1);
  }

  if ((date_arg && !time_arg) || (!date_arg && time_arg)) {
    fprintf(stdout, "Usage: TESTS NCP2225TIME file [datehex timehex]\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225TIME failed: current drive is not a network drive\n");
    return(1);
  }

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &before,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225TIME %s read-before rc=%d\n", path, rc);
    return(rc);
  }

  if (date_arg) {
    if (tests_parse_word_arg(date_arg, &target_date) ||
        tests_parse_word_arg(time_arg, &target_time)) {
      fprintf(stdout, "Bad date/time. Use DOS hex words, e.g. 5C99 A320.\n");
      return(1);
    }
  } else if (before.modify_date == target_date && before.modify_time == target_time) {
    target_date = 0x5c9a;  /* 2026-05-26 */
    target_time = 0x6c00;  /* 13:32:00 */
  }

  tests_print_dos_datetime("NCP2225TIME before", before.modify_date, before.modify_time);
  tests_print_dos_datetime("NCP2225TIME target", target_date, target_time);

  rc = tests_ncp22_25_set_mtime(dhandle, path, target_date, target_time);
  fprintf(stdout, "ncp22/25 set time rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &after,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225TIME %s read-after rc=%d\n", path, rc);
    return(rc);
  }
  tests_print_dos_datetime("NCP2225TIME after ", after.modify_date, after.modify_time);

  if (after.modify_date != target_date || after.modify_time != target_time) {
    fprintf(stdout, "NCP2225TIME failed: expected date=0x%04X time=0x%04X\n",
            (unsigned)target_date, (unsigned)target_time);
    return(1);
  }

  if (restore) {
    rc = tests_ncp22_25_set_mtime(dhandle, path, before.modify_date, before.modify_time);
    fprintf(stdout, "ncp22/25 restore time rc=%d neterrno=0x%02X\n",
            rc, (unsigned)(rc < 0 ? -rc : 0));
    if (rc)
      return(rc);

    rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                    (uint16)dhandle,
                                    &restored,
                                    NULL, NULL, NULL);
    if (rc) {
      fprintf(stdout, "NCP2225TIME %s read-restore rc=%d\n", path, rc);
      return(rc);
    }
    tests_print_dos_datetime("NCP2225TIME restored", restored.modify_date, restored.modify_time);
    if (restored.modify_date != before.modify_date || restored.modify_time != before.modify_time) {
      fprintf(stdout, "NCP2225TIME failed: restore mismatch\n");
      return(1);
    }
  }

  return(0);
}


static int tests_ncp2225adate(char *path, char *date_arg)
{
  uint8 dhandle = 0;
  NCP_NDIR_INFO before;
  NCP_NDIR_INFO after;
  NCP_NDIR_INFO restored;
  uint16 target_date = 0x5c99;  /* 2026-04-25 */
  int rc;
  int restore = 1;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225ADATE file [datehex]\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225ADATE failed: current drive is not a network drive\n");
    return(1);
  }

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &before,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225ADATE %s read-before rc=%d\n", path, rc);
    return(rc);
  }

  if (date_arg) {
    if (tests_parse_word_arg(date_arg, &target_date)) {
      fprintf(stdout, "Bad date. Use DOS hex word, e.g. 5C99.\n");
      return(1);
    }
  } else if (before.last_access_date == target_date) {
    target_date = 0x5c9a;  /* 2026-04-26 */
  }

  tests_print_dos_date("NCP2225ADATE before", before.last_access_date);
  tests_print_dos_date("NCP2225ADATE target", target_date);
  tests_print_dos_datetime("NCP2225ADATE mtime-before", before.modify_date, before.modify_time);

  rc = tests_ncp22_25_set_adate(dhandle, path, target_date);
  fprintf(stdout, "ncp22/25 set adate rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &after,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225ADATE %s read-after rc=%d\n", path, rc);
    return(rc);
  }
  tests_print_dos_date("NCP2225ADATE after ", after.last_access_date);
  tests_print_dos_datetime("NCP2225ADATE mtime-after ", after.modify_date, after.modify_time);

  if (after.last_access_date != target_date) {
    fprintf(stdout, "NCP2225ADATE failed: expected date=0x%04X\n",
            (unsigned)target_date);
    return(1);
  }

  if (after.modify_date != before.modify_date || after.modify_time != before.modify_time) {
    fprintf(stdout, "NCP2225ADATE failed: modify time changed unexpectedly\n");
    return(1);
  }

  if (restore) {
    rc = tests_ncp22_25_set_adate(dhandle, path, before.last_access_date);
    fprintf(stdout, "ncp22/25 restore adate rc=%d neterrno=0x%02X\n",
            rc, (unsigned)(rc < 0 ? -rc : 0));
    if (rc)
      return(rc);

    rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                    (uint16)dhandle,
                                    &restored,
                                    NULL, NULL, NULL);
    if (rc) {
      fprintf(stdout, "NCP2225ADATE %s read-restore rc=%d\n", path, rc);
      return(rc);
    }
    tests_print_dos_date("NCP2225ADATE restored", restored.last_access_date);
    if (restored.last_access_date != before.last_access_date) {
      fprintf(stdout, "NCP2225ADATE failed: restore mismatch\n");
      return(1);
    }
  }

  return(0);
}





static int tests_ncp2225maxspace(char *path, char *space_arg)
{
  uint8 dhandle = 0;
  uint32 before = 0;
  uint32 target = 0x00100000UL; /* NetWare 4K blocks: 4 GiB */
  int rc;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225MAXSPACE dir [blockshex]   (dir must be current-dir 8.3 name)\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225MAXSPACE failed: current drive is not a network drive\n");
    return(1);
  }

  rc = tests_ncp22_1e_read_dir_maxspace(dhandle, path, &before);
  if (rc) {
    before = 0x40000000UL;
    fprintf(stdout, "NCP2225MAXSPACE %s read-before rc=%d, using unlimited and continuing\n",
            path, rc);
  }

  if (space_arg) {
    if (tests_parse_dword_arg(space_arg, &target)) {
      fprintf(stdout, "Bad maximum-space '%s'. Use hex blocks, e.g. 100000 or 0x100000.\n", space_arg);
      return(1);
    }
  } else if (before == target) {
    target = 0x00200000UL; /* 8 GiB */
  }

  fprintf(stdout, "NCP2225MAXSPACE %s before=0x%08lX target=0x%08lX\n",
          path, (unsigned long)before, (unsigned long)target);

  rc = tests_ncp22_25_set_maxspace(dhandle, path, target);
  fprintf(stdout, "ncp22/25 set maxspace rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  /*
   * The old NCP22/1E maximum-space readback layout is not reliable with all
   * clients/servers; in particular it can return -51 although the NCP22/25
   * set call reached the server and completed successfully.  Keep this test
   * focused on the write path.  Verify the quota value through the server log
   * or host tools such as repquota/setquota when needed.
   */
  fprintf(stdout, "NCP2225MAXSPACE set-path OK; readback skipped\n");

  return(0);
}


static int tests_ncp2225modid(char *path, char *id_arg)
{
  uint8 dhandle = 0;
  NCP_NDIR_INFO before;
  NCP_NDIR_INFO after;
  NCP_NDIR_INFO restored;
  uint32 target_id = 0x00010003UL;
  int rc;
  int restore = 1;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225MODID file [idhex]\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225MODID failed: current drive is not a network drive\n");
    return(1);
  }

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &before,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225MODID %s read-before rc=%d\n", path, rc);
    return(rc);
  }

  if (id_arg) {
    target_id = strtoul(id_arg, NULL, 16);
  } else if (before.modifier_id == target_id) {
    target_id = 0x00000002UL;
  }

  fprintf(stdout, "NCP2225MODID before-id 0x%08lX\n", (unsigned long)before.modifier_id);
  fprintf(stdout, "NCP2225MODID target-id 0x%08lX\n", (unsigned long)target_id);

  rc = tests_ncp22_25_set_modifier(dhandle, path, target_id);
  fprintf(stdout, "ncp22/25 set modid rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &after,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225MODID %s read-after rc=%d\n", path, rc);
    return(rc);
  }

  fprintf(stdout, "NCP2225MODID after-id  0x%08lX\n", (unsigned long)after.modifier_id);

  if (after.modifier_id != target_id) {
    fprintf(stdout, "NCP2225MODID failed: modifier id mismatch\n");
    return(1);
  }

  if (restore) {
    rc = tests_ncp22_25_set_modifier(dhandle, path, before.modifier_id);
    fprintf(stdout, "ncp22/25 restore modid rc=%d neterrno=0x%02X\n",
            rc, (unsigned)(rc < 0 ? -rc : 0));
    if (rc)
      return(rc);

    rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                    (uint16)dhandle,
                                    &restored,
                                    NULL, NULL, NULL);
    if (rc) {
      fprintf(stdout, "NCP2225MODID %s read-restore rc=%d\n", path, rc);
      return(rc);
    }
    fprintf(stdout, "NCP2225MODID restored-id 0x%08lX\n", (unsigned long)restored.modifier_id);

    if (restored.modifier_id != before.modifier_id) {
      fprintf(stdout, "NCP2225MODID failed: restore mismatch\n");
      return(1);
    }
  }

  return(0);
}

static int tests_ncp2225create(char *path, char *date_arg, char *time_arg, char *id_arg)
{
  uint8 dhandle = 0;
  NCP_NDIR_INFO before;
  NCP_NDIR_INFO after;
  NCP_NDIR_INFO restored;
  uint16 target_date = 0x5c99;  /* 2026-04-25 */
  uint16 target_time = 0xa320;  /* 20:25:00 */
  uint32 target_id = 1;
  int rc;
  int restore = 1;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225CREATE file [datehex timehex idhex]\n");
    return(1);
  }

  if ((date_arg && (!time_arg || !id_arg)) || (!date_arg && (time_arg || id_arg))) {
    fprintf(stdout, "Usage: TESTS NCP2225CREATE file [datehex timehex idhex]\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225CREATE failed: current drive is not a network drive\n");
    return(1);
  }

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &before,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225CREATE %s read-before rc=%d\n", path, rc);
    return(rc);
  }

  if (date_arg) {
    if (tests_parse_word_arg(date_arg, &target_date) ||
        tests_parse_word_arg(time_arg, &target_time)) {
      fprintf(stdout, "Bad create date/time. Use DOS hex words, e.g. 5C99 A320 00000001.\n");
      return(1);
    }
    target_id = strtoul(id_arg, NULL, 16);
  } else if (before.creation_date == target_date && before.creation_time == target_time &&
             before.creator_id == target_id) {
    target_date = 0x5c9a;
    target_time = 0x6c00;
    target_id = 2;
  }

  tests_print_dos_datetime("NCP2225CREATE before", before.creation_date, before.creation_time);
  fprintf(stdout, "NCP2225CREATE before-id 0x%08lX\n", (unsigned long)before.creator_id);
  tests_print_dos_datetime("NCP2225CREATE target", target_date, target_time);
  fprintf(stdout, "NCP2225CREATE target-id 0x%08lX\n", (unsigned long)target_id);

  rc = tests_ncp22_25_set_create(dhandle, path, target_date, target_time, target_id);
  fprintf(stdout, "ncp22/25 set create rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &after,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225CREATE %s read-after rc=%d\n", path, rc);
    return(rc);
  }

  tests_print_dos_datetime("NCP2225CREATE after ", after.creation_date, after.creation_time);
  fprintf(stdout, "NCP2225CREATE after-id  0x%08lX\n", (unsigned long)after.creator_id);

  if (after.creation_date != target_date || after.creation_time != target_time ||
      after.creator_id != target_id) {
    fprintf(stdout, "NCP2225CREATE failed: create metadata mismatch\n");
    return(1);
  }

  if (restore) {
    rc = tests_ncp22_25_set_create(dhandle, path, before.creation_date,
                                   before.creation_time, before.creator_id);
    fprintf(stdout, "ncp22/25 restore create rc=%d neterrno=0x%02X\n",
            rc, (unsigned)(rc < 0 ? -rc : 0));
    if (rc)
      return(rc);

    rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                    (uint16)dhandle,
                                    &restored,
                                    NULL, NULL, NULL);
    if (rc) {
      fprintf(stdout, "NCP2225CREATE %s read-restore rc=%d\n", path, rc);
      return(rc);
    }
    tests_print_dos_datetime("NCP2225CREATE restored", restored.creation_date, restored.creation_time);
    fprintf(stdout, "NCP2225CREATE restored-id 0x%08lX\n", (unsigned long)restored.creator_id);

    if (restored.creation_date != before.creation_date ||
        restored.creation_time != before.creation_time ||
        restored.creator_id != before.creator_id) {
      fprintf(stdout, "NCP2225CREATE failed: restore mismatch\n");
      return(1);
    }
  }

  return(0);
}


static int tests_ncp2225arch(char *path, char *date_arg, char *time_arg, char *id_arg)
{
  uint8 dhandle = 0;
  NCP_NDIR_INFO before;
  NCP_NDIR_INFO after;
  NCP_NDIR_INFO restored;
  uint16 target_date = 0x5c99;  /* 2026-04-25 */
  uint16 target_time = 0xa320;  /* 20:25:00 */
  uint32 target_id = 1;
  int rc;
  int restore = 1;

  if (!path || !*path) {
    fprintf(stdout, "Usage: TESTS NCP2225ARCH file [datehex timehex idhex]\n");
    return(1);
  }

  if ((date_arg && (!time_arg || !id_arg)) || (!date_arg && (time_arg || id_arg))) {
    fprintf(stdout, "Usage: TESTS NCP2225ARCH file [datehex timehex idhex]\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP2225ARCH failed: current drive is not a network drive\n");
    return(1);
  }

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &before,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225ARCH %s read-before rc=%d\n", path, rc);
    return(rc);
  }

  if (date_arg) {
    if (tests_parse_word_arg(date_arg, &target_date) ||
        tests_parse_word_arg(time_arg, &target_time)) {
      fprintf(stdout, "Bad archive date/time. Use DOS hex words, e.g. 5C99 A320 00000001.\n");
      return(1);
    }
    target_id = strtoul(id_arg, NULL, 16);
  } else if (before.archive_date == target_date && before.archive_time == target_time &&
             before.archiver_id == target_id) {
    target_date = 0x5c9a;
    target_time = 0x6c00;
    target_id = 2;
  }

  tests_print_dos_datetime("NCP2225ARCH before", before.archive_date, before.archive_time);
  fprintf(stdout, "NCP2225ARCH before-id 0x%08lX\n", (unsigned long)before.archiver_id);
  tests_print_dos_datetime("NCP2225ARCH target", target_date, target_time);
  fprintf(stdout, "NCP2225ARCH target-id 0x%08lX\n", (unsigned long)target_id);

  rc = tests_ncp22_25_set_archive(dhandle, path, target_date, target_time, target_id);
  fprintf(stdout, "ncp22/25 set archive rc=%d neterrno=0x%02X\n",
          rc, (unsigned)(rc < 0 ? -rc : 0));
  if (rc)
    return(rc);

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &after,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP2225ARCH %s read-after rc=%d\n", path, rc);
    return(rc);
  }

  tests_print_dos_datetime("NCP2225ARCH after ", after.archive_date, after.archive_time);
  fprintf(stdout, "NCP2225ARCH after-id  0x%08lX\n", (unsigned long)after.archiver_id);

  if (after.archive_date != target_date || after.archive_time != target_time ||
      after.archiver_id != target_id) {
    fprintf(stdout, "NCP2225ARCH failed: archive metadata mismatch\n");
    return(1);
  }

  if (restore) {
    rc = tests_ncp22_25_set_archive(dhandle, path, before.archive_date,
                                    before.archive_time, before.archiver_id);
    fprintf(stdout, "ncp22/25 restore archive rc=%d neterrno=0x%02X\n",
            rc, (unsigned)(rc < 0 ? -rc : 0));
    if (rc)
      return(rc);

    rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                    (uint16)dhandle,
                                    &restored,
                                    NULL, NULL, NULL);
    if (rc) {
      fprintf(stdout, "NCP2225ARCH %s read-restore rc=%d\n", path, rc);
      return(rc);
    }
    tests_print_dos_datetime("NCP2225ARCH restored", restored.archive_date, restored.archive_time);
    fprintf(stdout, "NCP2225ARCH restored-id 0x%08lX\n", (unsigned long)restored.archiver_id);
  }

  return(0);
}

static int tests_ncp22_4_modify_irm(uint8 dhandle, char *path, uint8 mask)
{
  uint8 plen;

  if (!path)
    path = "";

  plen = (uint8)strlen(path);

  /*
   * NCP 22 / subfunction 4:
   *   byte subfunction = 0x04
   *   byte dir handle
   *   byte grant rights mask
   *   byte revoke rights mask
   *   byte path length
   *   path bytes
   *
   * The operation is modify-style, not absolute-set-style:
   *   new_mask = (old_mask | grant_mask) & ~revoke_mask
   * To make this TESTS command set an exact target mask, grant the bits
   * in the target and revoke all bits outside the target.
   * No reply payload is expected.
   */
  {
    struct {
      uint16 len;
      uint8  func;
      uint8  dirhandle;
      uint8  grant;
      uint8  revoke;
      uint8  pathlen;
      uint8  path[256];
    } req;
    struct {
      uint16 len;
    } repl = { 0 };

    req.func = 0x04;
    req.dirhandle = dhandle;
    req.grant = mask;
    req.revoke = (uint8)(~mask & 0xff);
    req.pathlen = plen;
    req.len = 5 + plen;
    strmaxcpy(req.path, path, plen);

    neterrno = Net_Call(0xE200, &req, &repl);
    if (neterrno)
      return(-neterrno);
    return(0);
  }
}


static int tests_put_ncp22_component_path(uint8 *dst, int max, char *path,
                                          uint8 *count_out)
{
  int pos = 0;
  int count = 0;
  char *p;

  if (!dst || max < 1 || !count_out)
    return(-1);

  if (!path)
    path = "";

  p = strchr(path, ':');
  if (p)
    path = p + 1;

  while (*path == '\\' || *path == '/')
    path++;

  while (*path) {
    char *start = path;
    int len;

    while (*path && *path != '\\' && *path != '/')
      path++;

    len = (int)(path - start);
    if (len > 0) {
      if (len > 255 || pos + 1 + len > max || count >= 255)
        return(-1);
      dst[pos++] = (uint8)len;
      memcpy(dst + pos, start, len);
      pos += len;
      count++;
    }

    while (*path == '\\' || *path == '/')
      path++;
  }

  *count_out = (uint8)count;
  return(pos);
}

static int tests_ncp22_2e_rename(uint8 dhandle, char *src, char *dst)
{
  uint8 srcbuf[260];
  uint8 dstbuf[260];
  uint8 srccount = 0;
  uint8 dstcount = 0;
  int srclen;
  int dstlen;

  srclen = tests_put_ncp22_component_path(srcbuf, sizeof(srcbuf), src, &srccount);
  dstlen = tests_put_ncp22_component_path(dstbuf, sizeof(dstbuf), dst, &dstcount);
  if (srclen < 0 || dstlen < 0)
    return(-1);

  {
    struct {
      uint16 len;
      uint8  func;
      uint8  source_dhandle;
      uint8  search_attributes;
      uint8  source_component_count;
      uint8  data[520];
    } req;
    struct {
      uint16 len;
    } repl = { 0 };
    int pos = 0;

    memset(&req, 0, sizeof(req));
    req.func = 0x2e;
    req.source_dhandle = dhandle;
    req.search_attributes = 0x06;
    req.source_component_count = srccount;
    memcpy(req.data + pos, srcbuf, srclen);
    pos += srclen;
    req.data[pos++] = dhandle;
    req.data[pos++] = dstcount;
    memcpy(req.data + pos, dstbuf, dstlen);
    pos += dstlen;
    req.len = (uint16)(4 + pos);

    neterrno = Net_Call(0xE200, &req, &repl);
    if (neterrno)
      return(-neterrno);
    return(0);
  }
}

static int tests_ncp22ren(char *src, char *dst)
{
  uint8 dhandle = 0;
  int rc;

  if (!src || !dst) {
    fprintf(stdout, "Usage: TESTS NCP22REN oldpath newpath\n");
    return(1);
  }

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP22REN failed: current drive is not a network drive\n");
    return(1);
  }

  fprintf(stdout, "NCP22/2E Rename Or Move old: %s -> %s\n", src, dst);
  fprintf(stdout, "Current dir handle: %u\n", (unsigned)dhandle);

  rc = tests_ncp22_2e_rename(dhandle, src, dst);
  fprintf(stdout, "ncp22/2e rc=%d\n", rc);
  return(rc ? rc : 0);
}

static void tests_join_child(char *out, int outlen, char *parent, char *child)
{
  int len;

  if (!parent || !*parent || tool_strsame(parent, ".")) {
    strmaxcpy(out, child, outlen - 1);
    return;
  }

  strmaxcpy(out, parent, outlen - 1);
  len = strlen(out);
  if (len > 0 && out[len - 1] != '\\' && out[len - 1] != ':' && len < outlen - 1) {
    out[len++] = '\\';
    out[len] = '\0';
  }
  strmaxcpy(out + len, child, outlen - len - 1);
}

static int tests_ncp22rendir(char *parent)
{
  uint8 dhandle = 0;
  char src[260];
  char dst[260];
  int rc;

  if (!parent || !*parent)
    parent = ".";

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP22RENDIR failed: current drive is not a network drive\n");
    return(1);
  }

  tests_join_child(src, sizeof(src), parent, "TREN");
  tests_join_child(dst, sizeof(dst), parent, "TREN2");

  fprintf(stdout, "NCP22/2E Directory Rename test: %s -> %s\n", src, dst);
  fprintf(stdout, "Current dir handle: %u\n", (unsigned)dhandle);
  fprintf(stdout, "Create the source directory first, e.g. MD %s\n", src);

  rc = tests_ncp22_2e_rename(dhandle, src, dst);
  fprintf(stdout, "ncp22/2e dir rc=%d\n", rc);
  /* return(rc ? rc : 0); */
  return(0);
}

static int tests_read_irm_by_ncp87(uint8 dhandle, char *path, uint8 *mask_out)
{
  NCP_NDIR_INFO info;
  int rc;

  if (mask_out)
    *mask_out = 0;

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &info,
                                  NULL, NULL, NULL);
  if (rc)
    return(rc);

  if (mask_out)
    *mask_out = (uint8)info.inherited_rights;
  return(0);
}

static int tests_ncp22s4(char *path, char *mask_arg)
{
  uint8 dhandle = 0;
  uint8 before = 0;
  uint8 after = 0;
  uint8 mask = 0;
  int rc;

  if (!path)
    path = ".";

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP22S4 failed: current drive is not a network drive\n");
    return(1);
  }

  fprintf(stdout, "NCP22/4 Modify Maximum/Inherit Rights Mask for %s\n", path);
  fprintf(stdout, "Current dir handle: %u\n", (unsigned)dhandle);

  rc = tests_read_irm_by_ncp87(dhandle, path, &before);
  tests_print_mask("before ncp87 irm", rc, before);

  if (!mask_arg)
    return(rc ? rc : 0);

  if (tests_parse_irm_mask(mask_arg, &mask)) {
    fprintf(stdout, "Bad mask '%s'. Use e.g. RF, 42, 0x42, ALL, NONE, SRWCEMFA.\\n",
            mask_arg);
    return(1);
  }

  tests_print_mask("target mask", 0, mask);

  rc = tests_ncp22_4_modify_irm(dhandle, path, mask);
  if (rc) {
    fprintf(stdout, "ncp22/4 rc=%d\\n", rc);
    return(rc);
  }

  fprintf(stdout, "ncp22/4 rc=0\\n");

  rc = tests_read_irm_by_ncp87(dhandle, path, &after);
  tests_print_mask("after ncp87 irm", rc, after);

  return(rc ? rc : 0);
}


static int tests_ncp87c32attr(void)
{
  uint8 dhandle = 0;
  uint32 attr = 0;
  uint16 actual = 0;
  uint16 handle_lo = 0;
  uint16 handle_hi = 0;
  int rc;

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP87C32ATTR failed: current drive is not a network drive\n");
    return(1);
  }

  rc = ncp87_06_obtain_rim_attributes("LOGIN.EXE",
                                       (uint16)dhandle,
                                       &attr,
                                       &actual,
                                       &handle_lo,
                                       &handle_hi);
  if (rc) {
    fprintf(stdout, "NCP87C32ATTR failed rc=%d\n", rc);
    return(rc);
  }

  fprintf(stdout, "NCP87C32ATTR LOGIN.EXE attr=%02lX handle=%04X:%04X actual=%04X\n",
          attr & 0xffUL, handle_hi, handle_lo, actual);

  return(0);
}

static int tests_ncp87c32auto(void)
{
  /*
   * Kept as a compatibility alias for the former verbose test command.
   * The production helper path is exercised by NCP87C32ATTR.
   */
  return tests_ncp87c32attr();
}



static void tests_print_reply_bytes(uint8 *data, int len)
{
  int i;
  int n = len;

  if (n < 0) n = 0;
  if (n > 16) n = 16;

  for (i = 0; i < n; i++)
    fprintf(stdout, "%02X", (unsigned)data[i]);
}

static void tests_print_ncp23_probe_row(char *label, int rc,
                                        uint16 repl_len, uint8 *repl_data)
{
  fprintf(stdout, "%-16.16s %5d %5u ", label, rc, (unsigned)repl_len);
  if (!rc)
    tests_print_reply_bytes(repl_data, repl_len);
  else
    fprintf(stdout, "-");
  fprintf(stdout, "\n");
}

static void tests_print_ncp23_path(uint8 *data, int len)
{
  int pos = 0;
  int first = 1;

  while (pos < len) {
    int l = data[pos++];
    int i;
    if (l < 0 || pos + l > len) {
      fprintf(stdout, " <bad-component>");
      return;
    }
    if (!first) fprintf(stdout, "\\");
    for (i = 0; i < l; i++)
      fputc(data[pos+i], stdout);
    pos += l;
    first = 0;
  }
}


static uint16 tests_ncp23_path_reply_len(uint8 *data, uint16 maxlen)
{
  uint16 pos = 0;

  while (pos < maxlen) {
    uint8 l = data[pos];

    /*
     * The DOS Net_Call reply buffer length is the buffer capacity here, not
     * the actual NCP payload length.  mars_nwe pads the unused part with
     * zeroes, while a valid F3 path component cannot have length zero.
     */
    if (!l)
      break;

    pos++;
    if ((uint16)(pos + l) > maxlen)
      return(maxlen);
    pos = (uint16)(pos + l);
  }

  return(pos);
}

static int tests_ncp23_f4(uint8 dhandle, char *path, uint16 *repl_len,
                           uint8 *repl_data, int repl_max)
{
  uint8 plen;
  struct {
    uint16 len;
    uint8  func;
    uint8  dhandle;
    uint8  pathlen;
    uint8  path[255];
  } req;
  struct {
    uint16 len;
    uint8  data[300];
  } repl;

  if (!path)
    path = "";
  plen = (uint8)strlen(path);

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  repl.len = sizeof(repl.data);

  req.func = 0xf4;
  req.dhandle = dhandle;
  req.pathlen = plen;
  memcpy(req.path, path, plen);
  req.len = 3 + plen;

  neterrno = Net_Call(0xE300, &req, &repl);
  if (repl_len)
    *repl_len = repl.len;
  if (repl_data && repl_max > 0) {
    int copy = repl.len;
    if (copy > repl_max) copy = repl_max;
    memcpy(repl_data, repl.data, copy);
  }
  if (neterrno)
    return(-neterrno);
  return(0);
}


static int tests_ncp23_f3(uint8 volume, uint32 dirnum, uint8 namespace,
                          uint16 *repl_len, uint8 *repl_data, int repl_max)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  volume;
    uint8  dirnum[4];
    uint8  namespace;
  } req;
  struct {
    uint16 len;
    uint8  data[300];
  } repl;

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  repl.len = sizeof(repl.data);

  req.func = 0xf3;
  req.volume = volume;
  U32_TO_32(dirnum, req.dirnum);
  req.namespace = namespace;
  req.len = 7;

  neterrno = Net_Call(0xE300, &req, &repl);
  if (repl_len)
    *repl_len = repl.len;
  if (repl_data && repl_max > 0) {
    int copy = repl.len;
    if (copy > repl_max) copy = repl_max;
    memcpy(repl_data, repl.data, copy);
  }
  if (neterrno)
    return(-neterrno);
  return(0);
}


static int tests_ncp22_1a(uint8 volume, uint16 dirnum,
                          uint16 *repl_len, uint8 *repl_data, int repl_max)
{
  struct {
    uint16 len;
    uint8  func;
    uint8  volume;
    uint8  dirnum[2];
  } req;
  struct {
    uint16 len;
    uint8  data[260];
  } repl;

  memset(&req, 0, sizeof(req));
  memset(&repl, 0, sizeof(repl));
  repl.len = sizeof(repl.data);

  req.func = 0x1a;
  req.volume = volume;
  U16_TO_BE16(dirnum, req.dirnum);
  req.len = 4;

  neterrno = Net_Call(0xE200, &req, &repl);
  if (repl_len)
    *repl_len = repl.len;
  if (repl_data && repl_max > 0) {
    int copy = repl.len;
    if (copy > repl_max) copy = repl_max;
    memcpy(repl_data, repl.data, copy);
  }
  if (neterrno)
    return(-neterrno);
  return(0);
}

static uint16 tests_ncp22_1a_reply_len(uint8 *data, uint16 maxlen)
{
  if (!data || maxlen < 1)
    return(0);
  if ((uint16)(data[0] + 1) > maxlen)
    return(maxlen);
  return((uint16)(data[0] + 1));
}

static void tests_print_ncp22_1a_path(uint8 *data, uint16 len)
{
  int i;
  int plen;

  if (!data || len < 1)
    return;

  plen = data[0];
  if (plen + 1 > len)
    plen = len - 1;

  for (i = 0; i < plen; i++)
    fputc(data[1 + i], stdout);
}

static int tests_ncp23map(char *path)
{
  uint8 dhandle = 0;
  NCP_NDIR_INFO info;
  int rc;
  uint16 repl_len;
  uint8 repl_data[300];
  int i;
  int num_count;
  uint32 f4_dirnum = 0;
  uint8 f4_volume = 0;
  int f4_ok = 0;
  uint32 nums[3];
  char *names[3];

  if (!path)
    path = ".";

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "NCP23MAP failed: current drive is not a network drive\n");
    return(1);
  }

  memset(&info, 0, sizeof(info));
  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &info,
                                  NULL, NULL, NULL);

  fprintf(stdout, "NCP23/F4+F3 layout probes for %s\n", path);
  fprintf(stdout, "Current dir handle: %u\n", (unsigned)dhandle);
  if (rc) {
    fprintf(stdout, "NCP87 info rc=%d; F3 probes will use zero dir numbers\n", rc);
    info.vol_number = 0;
    info.dir_ent_num = 0;
    info.dos_dir_num = 0;
  } else {
    fprintf(stdout, "NCP87 info vol=%lu dirEnt=%lu dosDir=%lu\n",
            (unsigned long)info.vol_number,
            (unsigned long)info.dir_ent_num,
            (unsigned long)info.dos_dir_num);
  }

  fprintf(stdout, "\nNCP23 map calls\n");
  fprintf(stdout, "%-12s %5s %5s %-12s %s\n", "call", "rc", "len", "reply-hex", "decoded");
  fprintf(stdout, "%-12s %5s %5s %-12s %s\n", "------------", "-----", "-----", "------------", "----------------");

  memset(repl_data, 0, sizeof(repl_data));
  repl_len = 0;
  rc = tests_ncp23_f4(dhandle, path, &repl_len, repl_data, sizeof(repl_data));
  fprintf(stdout, "%-12s %5d %5u ", "f4 path", rc,
          (!rc && repl_len >= 5) ? 5U : (unsigned)repl_len);
  if (!rc && repl_len >= 5) {
    f4_ok = 1;
    f4_volume = repl_data[0];
    f4_dirnum = GET_32(repl_data + 1);
    tests_print_reply_bytes(repl_data, 5);
    fprintf(stdout, " vol=%u dir=%lu", (unsigned)f4_volume, (unsigned long)f4_dirnum);
  } else {
    fprintf(stdout, "-           ");
  }
  fprintf(stdout, "\n");

  if (f4_ok && f4_dirnum <= 0xffffUL) {
    memset(repl_data, 0, sizeof(repl_data));
    repl_len = 0;
    rc = tests_ncp22_1a(f4_volume, (uint16)f4_dirnum,
                        &repl_len, repl_data, sizeof(repl_data));
    if (!rc)
      repl_len = tests_ncp22_1a_reply_len(repl_data, repl_len);
    fprintf(stdout, "%-12s %5d %5u ", "22/1A f4", rc, (unsigned)repl_len);
    if (!rc) {
      tests_print_reply_bytes(repl_data, repl_len);
      fprintf(stdout, " ");
      tests_print_ncp22_1a_path(repl_data, repl_len);
    } else {
      fprintf(stdout, "-           ");
    }
    fprintf(stdout, "\n");
  }

  num_count = 0;
  if (f4_ok) {
    nums[num_count] = f4_dirnum;
    names[num_count++] = "f3 f4dir";
  }
  nums[num_count] = info.dir_ent_num;
  names[num_count++] = "f3 dirEnt";
  nums[num_count] = info.dos_dir_num;
  names[num_count++] = "f3 dosDir";

  for (i = (f4_ok ? 1 : 0); i < num_count; i++) {
    if (nums[i] > 0xffffUL)
      continue;
    memset(repl_data, 0, sizeof(repl_data));
    repl_len = 0;
    rc = tests_ncp22_1a((uint8)info.vol_number, (uint16)nums[i],
                        &repl_len, repl_data, sizeof(repl_data));
    if (!rc)
      repl_len = tests_ncp22_1a_reply_len(repl_data, repl_len);
    fprintf(stdout, "%-12s %5d %5u ",
            (i == (f4_ok ? 1 : 0)) ? "22/1A dir" : "22/1A dos",
            rc, (unsigned)repl_len);
    if (!rc) {
      tests_print_reply_bytes(repl_data, repl_len);
      fprintf(stdout, " ");
      tests_print_ncp22_1a_path(repl_data, repl_len);
    } else {
      fprintf(stdout, "-           ");
    }
    fprintf(stdout, "\n");
  }

  for (i = 0; i < num_count; i++) {
    memset(repl_data, 0, sizeof(repl_data));
    repl_len = 0;
    rc = tests_ncp23_f3((uint8)info.vol_number, nums[i], 0,
                        &repl_len, repl_data, sizeof(repl_data));
    if (!rc)
      repl_len = tests_ncp23_path_reply_len(repl_data, repl_len);
    fprintf(stdout, "%-12s %5d %5u ", names[i], rc, (unsigned)repl_len);
    if (!rc) {
      tests_print_reply_bytes(repl_data, repl_len);
      fprintf(stdout, " ");
      tests_print_ncp23_path(repl_data, repl_len);
    } else {
      fprintf(stdout, "-           ");
    }
    fprintf(stdout, "\n");
  }

  fprintf(stdout, "\nExpected F4 reply is: volume byte + directory number dword (LO-HI).\n");
  fprintf(stdout, "Expected F3 reply is: length-prefixed path components without volume.\n");
  fprintf(stdout, "Expected 22/1A reply is: length byte + DOS path string.\n");
  return(0);
}


static int tests_ncp22_eff_variant(int subfn, int variant,
                                   uint8 dhandle, char *path,
                                   uint8 *rights_out)
{
  uint8 plen;
  int subdir = 1;

  if (!path)
    path = "";

  plen = (uint8)strlen(path);
  if (rights_out)
    *rights_out = 0;

  /*
   * Exploratory old NCP22 directory service calls.
   *
   * subfn 3  = Get Effective Directory Rights
   * subfn 42 = Get Effective Rights           (SDK symbol NWNCP22S42...)
   *
   * The exact DOS requester wrapper layout is not documented in our tree, so
   * TESTS tries a few known 2.x/3.x style request shapes and reports which
   * one succeeds.  We do not use these in production until the matching
   * Novell RIGHTS shape is identified.
   */
  if (variant == 0) {
    struct {
      uint16 len;
      uint8  func;
      uint8  dirhandle;
      uint8  pathlen;
      uint8  path[256];
    } req;
    struct {
      uint16 len;
      uint8  rights;
    } repl = { sizeof(repl) - sizeof(uint16) };

    req.func = (uint8)subfn;
    req.dirhandle = dhandle;
    req.pathlen = plen;
    req.len = 3 + plen;
    strmaxcpy(req.path, path, plen);

    neterrno = Net_Call(0xE200, &req, &repl);
    if (neterrno)
      return(-neterrno);
    if (rights_out)
      *rights_out = repl.rights;
    return(0);
  }

  if (variant == 1) {
    struct {
      uint16 len;
      uint8  func;
      uint8  dirhandle;
      uint8  sub_dir[2];
      uint8  pathlen;
      uint8  path[256];
    } req;
    struct {
      uint16 len;
      uint8  rights;
    } repl = { sizeof(repl) - sizeof(uint16) };

    req.func = (uint8)subfn;
    req.dirhandle = dhandle;
    U16_TO_BE16(subdir, req.sub_dir);
    req.pathlen = plen;
    req.len = 5 + plen;
    strmaxcpy(req.path, path, plen);

    neterrno = Net_Call(0xE200, &req, &repl);
    if (neterrno)
      return(-neterrno);
    if (rights_out)
      *rights_out = repl.rights;
    return(0);
  }

  if (variant == 2) {
    struct {
      uint16 len;
      uint8  func;
      uint8  pathlen;
      uint8  path[256];
    } req;
    struct {
      uint16 len;
      uint8  rights;
    } repl = { sizeof(repl) - sizeof(uint16) };

    req.func = (uint8)subfn;
    req.pathlen = plen;
    req.len = 2 + plen;
    strmaxcpy(req.path, path, plen);

    neterrno = Net_Call(0xE200, &req, &repl);
    if (neterrno)
      return(-neterrno);
    if (rights_out)
      *rights_out = repl.rights;
    return(0);
  }

  return(-999);
}


static int tests_ncp22_50_obj_eff(uint32 object_id, uint8 dhandle,
                                  char *path, uint16 *rights_out)
{
  uint8 plen;

  if (!path)
    path = "";

  plen = (uint8)strlen(path);
  if (rights_out)
    *rights_out = 0;

  /*
   * Client32/NCPWIN32 exports this as:
   *   NWNCP22s50GetObjEffectRights
   *
   * Disassembly of NCPWIN32.DLL shows request shape:
   *   word len = 7 + pathlen
   *   byte subfunction = 0x32
   *   dword object id, big endian
   *   byte dir handle
   *   byte path length
   *   path bytes
   *
   * Reply is a little-endian word effective rights mask.
   */
  {
    struct {
      uint16 len;
      uint8  func;
      uint8  object_id[4];
      uint8  dirhandle;
      uint8  pathlen;
      uint8  path[256];
    } req;
    struct {
      uint16 len;
      uint8  rights[2];
    } repl = { sizeof(repl) - sizeof(uint16) };

    req.func = 0x32;
    U32_TO_BE32(object_id, req.object_id);
    req.dirhandle = dhandle;
    req.pathlen = plen;
    req.len = 7 + plen;
    strmaxcpy(req.path, path, plen);

    neterrno = Net_Call(0xE200, &req, &repl);
    if (neterrno)
      return(-neterrno);

    if (rights_out)
      *rights_out = (uint16)(repl.rights[0] | ((uint16)repl.rights[1] << 8));
    return(0);
  }
}

static void tests_print_ncp22_obj50_row(char *label, uint32 object_id,
                                        uint8 dhandle, char *path)
{
  uint16 rights = 0;
  uint8 mask = 0;
  int rc;

  rc = tests_ncp22_50_obj_eff(object_id, dhandle, path, &rights);
  if (!rc)
    mask = tests_map_ncp_mask(rights);
  tests_print_eff_row(label, rc, mask, rights, !rc);
}

static void tests_print_ncp22_row(char *label, int subfn, int variant,
                                  uint8 dhandle, char *path)
{
  uint8 rights = 0;
  int rc;

  rc = tests_ncp22_eff_variant(subfn, variant, dhandle, path, &rights);
  tests_print_eff_row(label, rc, rights, rights, !rc);
}

static int tests_effright(char *path)
{
  uint8 dhandle = 0;
  uint8 eff_old = 0;
  uint8 mask = 0;
  char usepath[260];
  int newhandle;
  uint16 ncp_rights = 0;
  NCP_NDIR_INFO info;
  uint32 my_obj_id = 0;
  uint16 my_obj_type = 0;
  uint8 my_obj_name[48];
  int rc;

  if (!path)
    path = ".";

  if (tool_current_dhandle_only(&dhandle)) {
    fprintf(stdout, "EFFRIGHT failed: current drive is not a network drive\n");
    return(1);
  }

  fprintf(stdout, "EFFRIGHT diagnostics for %s\n", path);
  fprintf(stdout, "Current dir handle: %u\n", (unsigned)dhandle);

  {
    int access_level;

    access_level = ncp14_46_get_bindery_access_level(&my_obj_id);
    my_obj_name[0] = '\0';
    if (access_level >= 0 && my_obj_id) {
      ncp17_36_get_bindery_object_name(my_obj_id, my_obj_name, &my_obj_type);
      fprintf(stdout, "Current object: %08lX type=%04X access=%02X name=%s\n\n",
              (unsigned long)my_obj_id, my_obj_type,
              (unsigned)access_level, my_obj_name);
    } else {
      fprintf(stdout, "Current object: unknown rc=%d\n\n", access_level);
    }
  }

  rc = ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &info,
                                  NULL, NULL, NULL);
  if (rc) {
    fprintf(stdout, "NCP87 info          rc=%d\n\n", rc);
  } else {
    tests_mask_string((uint8)info.inherited_rights, usepath);
    fprintf(stdout, "NCP87 info          vol=%lu dirEnt=%lu dosDir=%lu inh=[%s] %04X\n\n",
            (unsigned long)info.vol_number,
            (unsigned long)info.dir_ent_num,
            (unsigned long)info.dos_dir_num,
            usepath,
            info.inherited_rights);
  }

  tests_print_eff_header("Effective rights matrix:");

  rc = ncp87_1d_get_effective_rights(tool_is_current_path(path) ? "" : path,
                                      (uint16)dhandle,
                                      &ncp_rights,
                                      NULL, NULL, NULL);
  if (!rc)
    mask = tests_map_ncp_mask(ncp_rights);
  tests_print_eff_row("ncp87 path", rc, mask, ncp_rights, !rc);

  if (!ncp87_06_obtain_ndir_info(tool_is_current_path(path) ? "" : path,
                                  (uint16)dhandle,
                                  &info,
                                  NULL, NULL, NULL)) {
    rc = ncp87_1d_get_effective_rights_by_dirent((uint8)info.vol_number,
                                                  info.dos_dir_num,
                                                  &ncp_rights,
                                                  NULL, NULL, NULL);
    if (!rc)
      mask = tests_map_ncp_mask(ncp_rights);
    tests_print_eff_row("ncp87 dosDir", rc, mask, ncp_rights, !rc);

    rc = ncp87_1d_get_effective_rights_by_dirent((uint8)info.vol_number,
                                                  info.dir_ent_num,
                                                  &ncp_rights,
                                                  NULL, NULL, NULL);
    if (!rc)
      mask = tests_map_ncp_mask(ncp_rights);
    tests_print_eff_row("ncp87 dirEnt", rc, mask, ncp_rights, !rc);
  }

  /* Old handle path used by our earlier RIGHTS fallback. */
  tool_upcopy(usepath, path, sizeof(usepath));
  newhandle = alloc_temp_dir_handle(dhandle, usepath, 0, &eff_old);
  if (newhandle >= 0) {
    dealloc_dir_handle(newhandle);
    tests_print_eff_row("old handle path", 0, eff_old, eff_old, 1);
  } else {
    tests_print_eff_row("old handle path", newhandle, 0, 0, 0);
  }

  tool_parent_path(usepath, path, sizeof(usepath));
  newhandle = alloc_temp_dir_handle(dhandle, usepath, 0, &eff_old);
  if (newhandle >= 0) {
    dealloc_dir_handle(newhandle);
    tests_print_eff_row("old handle parent", 0, eff_old, eff_old, 1);
  } else {
    tests_print_eff_row("old handle parent", newhandle, 0, 0, 0);
  }

  if (usepath[0]) {
    int subdir = 1;
    int r = ncp16_02_get_directory_entry(dhandle, (uint8 *)usepath, &subdir,
                      NULL, NULL, NULL);
    if (r >= 0)
      tests_print_eff_row("ncp16_02 parent", 0, (uint8)r, (uint16)r, 1);
    else
      tests_print_eff_row("ncp16_02 parent", r, 0, 0, 0);
  }

  tests_print_ncp22_row("ncp22/3 v0", 3, 0, dhandle, path);
  tests_print_ncp22_row("ncp22/3 v1", 3, 1, dhandle, path);
  tests_print_ncp22_row("ncp22/42 v0", 42, 0, dhandle, path);
  tests_print_ncp22_row("ncp22/42 v1", 42, 1, dhandle, path);

  if (my_obj_id)
    tests_print_ncp22_obj50_row("obj50 path", my_obj_id, dhandle, path);

  if (usepath[0]) {
    tests_print_ncp22_row("p ncp22/3 v0", 3, 0, dhandle, usepath);
    tests_print_ncp22_row("p ncp22/3 v1", 3, 1, dhandle, usepath);
    tests_print_ncp22_row("p ncp22/42 v0", 42, 0, dhandle, usepath);
    tests_print_ncp22_row("p ncp22/42 v1", 42, 1, dhandle, usepath);

    if (my_obj_id)
      tests_print_ncp22_obj50_row("p obj50 path", my_obj_id, dhandle, usepath);
  }

  fprintf(stdout, "\nCompare with Novell: NPUBLIC\\RIGHTS %s\n", path);
  return(0);
}

int func_tests(int argc, char *argv[], int mode)
{
  (void)mode;

  if (argc < 2) {
    tests_usage();
    return(1);
  }

  if (tool_strsame(argv[1], "NCP87C32ATTR"))
    return tests_ncp87c32attr();

  if (tool_strsame(argv[1], "NCP87C32AUTO"))
    return tests_ncp87c32auto();

  if (tool_strsame(argv[1], "EFFRIGHT")) {
    if (argc < 3)
      return tests_effright(".");
    return tests_effright(argv[2]);
  }

  if (tool_strsame(argv[1], "NCP23MAP") ||
      tool_strsame(argv[1], "NCP23F")) {
    if (argc < 3)
      return tests_ncp23map(".");
    return tests_ncp23map(argv[2]);
  }

  if (tool_strsame(argv[1], "NCP22REN") ||
      tool_strsame(argv[1], "NCP22S2E")) {
    if (argc < 3)
      return tests_ncp22ren(NULL, NULL);
    if (argc < 4)
      return tests_ncp22rendir(argv[2]);
    return tests_ncp22ren(argv[2], argv[3]);
  }

  if (tool_strsame(argv[1], "RENDIR") ||
      tool_strsame(argv[1], "DIRREN") ||
      tool_strsame(argv[1], "NCP22RENDIR") ||
      tool_strsame(argv[1], "NCP22DIRREN")) {
    if (argc < 3)
      return tests_ncp22rendir(".");
    return tests_ncp22rendir(argv[2]);
  }

  if (tool_strsame(argv[1], "NCP2225MISS")) {
    if (argc < 3)
      return tests_ncp2225miss(NULL);
    return tests_ncp2225miss(argv[2]);
  }

  if (tool_strsame(argv[1], "NCP2225ATTR")) {
    if (argc < 3)
      return tests_ncp2225attr(NULL, NULL);
    if (argc < 4)
      return tests_ncp2225attr(argv[2], NULL);
    return tests_ncp2225attr(argv[2], argv[3]);
  }

  if (tool_strsame(argv[1], "NCP2225TIME")) {
    if (argc < 3)
      return tests_ncp2225time(NULL, NULL, NULL);
    if (argc < 5)
      return tests_ncp2225time(argv[2], NULL, NULL);
    return tests_ncp2225time(argv[2], argv[3], argv[4]);
  }

  if (tool_strsame(argv[1], "NCP2225ADATE")) {
    if (argc < 3)
      return tests_ncp2225adate(NULL, NULL);
    if (argc < 4)
      return tests_ncp2225adate(argv[2], NULL);
    return tests_ncp2225adate(argv[2], argv[3]);
  }

  if (tool_strsame(argv[1], "NCP2225MAXSPACE")) {
    if (argc < 3)
      return tests_ncp2225maxspace(NULL, NULL);
    if (argc < 4)
      return tests_ncp2225maxspace(argv[2], NULL);
    return tests_ncp2225maxspace(argv[2], argv[3]);
  }

  if (tool_strsame(argv[1], "NCP221EINFO")) {
    if (argc < 3)
      return tests_ncp221einfo(".");
    return tests_ncp221einfo(argv[2]);
  }

  if (tool_strsame(argv[1], "NCP2225MODID")) {
    if (argc < 3)
      return tests_ncp2225modid(NULL, NULL);
    if (argc < 4)
      return tests_ncp2225modid(argv[2], NULL);
    return tests_ncp2225modid(argv[2], argv[3]);
  }

  if (tool_strsame(argv[1], "NCP2225CREATE")) {
    if (argc < 3)
      return tests_ncp2225create(NULL, NULL, NULL, NULL);
    if (argc < 6)
      return tests_ncp2225create(argv[2], NULL, NULL, NULL);
    return tests_ncp2225create(argv[2], argv[3], argv[4], argv[5]);
  }

  if (tool_strsame(argv[1], "NCP2225ARCH")) {
    if (argc < 3)
      return tests_ncp2225arch(NULL, NULL, NULL, NULL);
    if (argc < 6)
      return tests_ncp2225arch(argv[2], NULL, NULL, NULL);
    return tests_ncp2225arch(argv[2], argv[3], argv[4], argv[5]);
  }

  if (tool_strsame(argv[1], "NCP22S4") ||
      tool_strsame(argv[1], "MAXRIGHT") ||
      tool_strsame(argv[1], "IRM")) {
    if (argc < 3)
      return tests_ncp22s4(".", NULL);
    if (argc < 4)
      return tests_ncp22s4(argv[2], NULL);
    return tests_ncp22s4(argv[2], argv[3]);
  }

  tests_usage();
  return(1);
}
