/*
 * 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: REVOKE utility for deleting trustee assignments from NetWare directories and files.
 * Depends on: net.h, ncpapi.h, trustee.h/trustee.c trustee helpers, netcall.c requester helpers, tools.c shared utility routines.
 */

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

#ifndef _A_NORMAL
#define _A_NORMAL 0x00
#endif

static int revoke_last_rc = 0;

static void revoke_usage_short(int leading_blank, int bell_first)
{
  if (bell_first)
    fprintf(stdout, "\a");
  if (leading_blank || bell_first)
    fprintf(stdout, "\n");

  fprintf(stdout, "Usage:    REVOKE rightslist* [FOR path] FROM [USER|GROUP] name [options]\n");
  fprintf(stdout, "Options:  /SubDirectories | /Files\n");
  fprintf(stdout, " ");
  if (bell_first)
    fprintf(stdout, "\n");
}

static void revoke_usage_full(int leading_blank, int bell_before_tail)
{
  if (leading_blank)
    fprintf(stdout, "\n");

  fprintf(stdout, "Usage:    REVOKE rightslist* [FOR path] FROM [USER|GROUP] name [options]\n");
  fprintf(stdout, "Options:  /SubDirectories | /Files\n");
  fprintf(stdout, " \n");
  fprintf(stdout, "286 Rights:\t\t386 Rights:\n");
  fprintf(stdout, "---------------\t\t--------------------\n");
  fprintf(stdout, "ALL = All\t\tALL = All\n");
  fprintf(stdout, "R   = Read\t\tS   = Supervisor\n");
  fprintf(stdout, "W   = Write\t\tR   = Read\n");
  fprintf(stdout, "O   = Open\t\tW   = Write\n");
  fprintf(stdout, "C   = Create\t\tC   = Create\n");
  fprintf(stdout, "D   = Delete\t\tE   = Erase\n");
  fprintf(stdout, "P   = Parental\t\tM   = Modify\n");
  fprintf(stdout, "S   = Search\t\tF   = File Scan\n");
  fprintf(stdout, "M   = Modify\t\tA   = Access Control\n");
  fprintf(stdout, "\n");
  if (bell_before_tail)
    fprintf(stdout, "\a");
  fprintf(stdout, "* Use abbreviations listed above, separated by spaces.\n");
}

static int revoke_one(char *path, uint16 dhandle, uint32 object_id,
                      uint16 revoke_mask, int forced_is_file)
{
  uint16 old_rights = 0;
  uint16 new_rights;
  int is_dir;
  int rc;

  /* Without /FILES, Novell REVOKE reports directory-oriented messages even
   * if the path cannot be located. */
  is_dir = forced_is_file ? 0 : 1;

  rc = ncp87_05_find_trustee_rights(path, dhandle, object_id, &old_rights,
                                     NULL, NULL, NULL);
  revoke_last_rc = rc;
  if (rc) {
    if (rc == 0xff)
      fprintf(stdout, "\aNo trustee for the specified %s.\n", is_dir ? "directory" : "file");
    else
      fprintf(stdout, "\aError scanning trustee list.\n");
    return(1);
  }

  new_rights = (uint16)(old_rights & ~revoke_mask);

  if (new_rights == 0) {
    /* Novell REVOKE removes the trustee through the old NCP22 trustee path.
     * Keep the Client32/NCP87 delete call as a compatibility fallback. */
    rc = ncp22_2b_delete_trustee_rights(path, dhandle, object_id);
    if (rc)
      rc = ncp87_0b_delete_trustee_rights(path, dhandle, object_id,
                                           NULL, NULL, NULL);
    revoke_last_rc = rc;
    if (rc) {
      fprintf(stdout, "Error deleting trustee.\n");
      return(1);
    }
  } else {
    /* Updating the remaining trustee mask is the same old SetTrustee flow
     * used by GRANT.  Fall back to NCP87 add-trustee if a server/client
     * does not accept NCP22/27. */
    rc = ncp22_27_set_trustee_rights(path, dhandle, object_id, new_rights);
    if (rc)
      rc = ncp87_0a_add_trustee_rights(path, dhandle, object_id, new_rights,
                                        0xffff, NULL, NULL, NULL);
    revoke_last_rc = rc;
    if (rc) {
      fprintf(stdout, "Fatal error revoking access rights.\n");
      return(1);
    }
  }

  {
    char header[300];
    char bracket[10];
    trustee_header_path(header, path, sizeof(header));
    trustee_rights_bracket(new_rights, bracket);
    fprintf(stdout, "%s\n", header);
    fprintf(stdout, "Trustee's access rights set to [%s]\r\n\n", bracket);
  }

  return(0);
}

static int revoke_subdirs_inner(char *path, uint16 dhandle, uint32 object_id,
                                uint16 revoke_mask, int *count,
                                int include_this)
{
  struct find_t ff;
  char pattern[260];
  char child[260];

  /* Novell REVOKE /SUBDIRECTORIES follows the REMOVE behaviour: the
   * specified start directory is a container for the recursive operation,
   * not itself part of the operation.  Once a listed subdirectory reports
   * no direct trustee, Novell stops after that first failure. */
  if (include_this) {
    if (revoke_one(path, dhandle, object_id, revoke_mask, 0) == 0)
      (*count)++;
    else
      return(1);
  }

  tool_join_path(pattern, path, "*.*", sizeof(pattern));

  if (_dos_findfirst(pattern, _A_SUBDIR, &ff) == 0) {
    do {
      if ((ff.attrib & _A_SUBDIR) && !tool_is_dot_dir(ff.name)) {
        tool_join_path(child, path, ff.name, sizeof(child));
        if (revoke_subdirs_inner(child, dhandle, object_id, revoke_mask,
                                 count, 1))
          return(1);
      }
    } while (_dos_findnext(&ff) == 0);
  }

  return(0);
}

static int revoke_subdirs(char *path, uint16 dhandle, uint32 object_id,
                          uint16 revoke_mask, int *count)
{
  return(revoke_subdirs_inner(path, dhandle, object_id, revoke_mask,
                              count, 0));
}

static int revoke_files(char *path, uint16 dhandle, uint32 object_id,
                        uint16 revoke_mask, int *count)
{
  struct find_t ff;
  char dir[260];
  char pat[80];
  char spec[260];
  char target[260];
  int rc = 0;

  if (tool_path_is_dir(path)) {
    strmaxcpy(dir, path, sizeof(dir) - 1);
    strmaxcpy(pat, "*.*", sizeof(pat) - 1);
  } else if (tool_has_wildcards(path)) {
    tool_parent_pattern(dir, pat, path, sizeof(dir), sizeof(pat));
  } else {
    if (revoke_one(path, dhandle, object_id, revoke_mask, 1) == 0)
      (*count)++;
    else
      rc = 1;
    return(rc);
  }

  tool_join_path(spec, dir, pat, sizeof(spec));
  if (_dos_findfirst(spec, _A_NORMAL | _A_HIDDEN | _A_SYSTEM | _A_ARCH, &ff) == 0) {
    do {
      if (!(ff.attrib & _A_SUBDIR)) {
        tool_join_path(target, dir, ff.name, sizeof(target));
        if (revoke_one(target, dhandle, object_id, revoke_mask, 1) == 0)
          (*count)++;
        else
          rc = 1;
      }
    } while (_dos_findnext(&ff) == 0);
  }

  return(rc);
}

int func_revoke(int argc, char *argv[], int mode)
{
  uint16 rights = 0;
  char *path = ".";
  char *objname = NULL;
  char objprint[48];
  uint16 objtype = TRUSTEE_BINDERY_USER;
  int objtype_given = 0;
  uint8 connid = 0;
  uint8 dhandle = 0;
  uint32 object_id;
  int use_subdirs = 0;
  int use_files = 0;
  int count = 0;
  int i = 1;
  int have_rights = 0;
  int rc;

  (void)mode;

  if (argc < 2 || tool_is_help_arg(argv[1])) {
    if (argc < 2)
      revoke_usage_full(1, 1);
    else
      revoke_usage_short(1, 0);
    return(argc < 2 ? 1 : 0);
  }

  while (i < argc) {
    if (tool_strsame(argv[i], "FOR") || tool_strsame(argv[i], "FROM"))
      break;
    if (tool_is_option(argv[i]))
      break;
    if (trustee_parse_right_word(argv[i], &rights)) {
      fprintf(stdout, "Invalid right specified.\n\n");
      revoke_usage_full(0, 1);
      return(1);
    }
    have_rights = 1;
    i++;
  }

  if (!have_rights || i >= argc) {
    revoke_usage_short(0, 1);
    return(1);
  }

  if (tool_strsame(argv[i], "FOR")) {
    i++;
    if (i >= argc) {
      revoke_usage_short(0, 1);
      return(1);
    }
    path = argv[i++];
  }

  if (i >= argc || !tool_strsame(argv[i], "FROM")) {
    revoke_usage_short(0, 1);
    return(1);
  }
  i++;

  if (i < argc && tool_strsame(argv[i], "USER")) {
    objtype = TRUSTEE_BINDERY_USER;
    objtype_given = 1;
    i++;
  } else if (i < argc && tool_strsame(argv[i], "GROUP")) {
    objtype = TRUSTEE_BINDERY_GROUP;
    objtype_given = 1;
    i++;
  }

  if (i >= argc) {
    revoke_usage_short(0, 1);
    return(1);
  }

  objname = argv[i++];

  while (i < argc) {
    if (!tool_is_option(argv[i])) {
      revoke_usage_short(0, 1);
      return(1);
    }

    if (tool_is_files_option(argv[i])) {
      use_files = 1;
      i++;
      continue;
    }

    if (tool_is_subdirs_option(argv[i])) {
      use_subdirs = 1;
      i++;
      continue;
    }

    revoke_usage_short(0, 1);
    return(1);
  }

  /* Novell REVOKE does not emit a grammar-specific message for /FILES plus
   * /SUBDIRECTORIES.  It reports the same directory trustee failure text. */

  if (tool_current_dhandle(&connid, &dhandle)) {
    fprintf(stdout, "Error: Drive not mapped to Network.\n");
    return(1);
  }

  object_id = trustee_lookup_object(objname, &objtype, objtype_given);
  if (!object_id) {
    if (objtype_given && objtype == TRUSTEE_BINDERY_GROUP)
      fprintf(stdout, "\aGroup \"%s\" not found.\n", objname);
    else if (objtype_given)
      fprintf(stdout, "\aUser \"%s\" not found.\n", objname);
    else
      fprintf(stdout, "\aUser or group \"%s\" not found.\n", objname);
    return(1);
  }

  tool_upcopy(objprint, objname, sizeof(objprint));

  if (use_files && use_subdirs) {
    fprintf(stdout, "\aNo trustee for the specified directory.\n");
    revoke_last_rc = 0xff;
    return(1);
  } else if (use_subdirs)
    rc = revoke_subdirs(path, (uint16)dhandle, object_id, rights, &count);
  else if (use_files)
    rc = revoke_files(path, (uint16)dhandle, object_id, rights, &count);
  else {
    rc = revoke_one(path, (uint16)dhandle, object_id, rights, 0);
    if (!rc)
      count = 1;
  }

  if ((use_subdirs || (!use_files && !rc)) && count > 0)
    fprintf(stdout, "Rights for %d directories were changed for %s.\n\n", count, objprint);
  else if (use_files && count > 0)
    fprintf(stdout, "Rights for %d files were changed for %s.\n\n", count, objprint);

  return(rc ? (revoke_last_rc ? revoke_last_rc : 1) : 0);
}
