/* File safety checks.
   Copyright (C) 1999-2025 Free Software Foundation, Inc.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General
   Public License along with this library.  If not, see 
   <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <mailutils/types.h>
#include <mailutils/errno.h>
#include <mailutils/mu_auth.h>
#include <mailutils/list.h>
#include <mailutils/util.h>

/* Functions for checking mode of a file and the directory it resides in.
   Each of these checks certain bits and returns 0 if they are OK
   and non-0 otherwise. */

struct file_check_buffer
{
  struct stat filst;   /* Actual file stat. */
  int islink;          /* True if file is a symlink. */
  struct stat dirst;   /* Directory stat. */
  int cdir;            /* True if dirst is initialized. */
};

static int
_check_linkwrdir (struct file_check_buffer *fb)
{
  return ((fb->islink || fb->filst.st_nlink > 1) &&
	  (fb->dirst.st_mode & (S_IWGRP | S_IWOTH)));
}

static int
_check_gwrdir (struct file_check_buffer *fb)
{
  return fb->dirst.st_mode & S_IWGRP;
}

static int
_check_awrdir (struct file_check_buffer *fb)
{
  return fb->dirst.st_mode & S_IWOTH;
}

/* The table of permission checkers below has this type: */
struct safety_checker
{
  char *name;            /* Symbolic name */
  int flag;              /* MU_FILE_SAFETY_ flag that enables this entry */
  int err;               /* Corresponding error code */
  int mode;              /* Corresponding file mode bit */
  int cdir;              /* True if the function needs dirst member */
  int (*fun) (struct file_check_buffer *fb); /* Checker function */
};

static struct safety_checker file_safety_check_tab[] = {
  { "grdfil", MU_FILE_SAFETY_GROUP_READABLE, MU_ERR_PERM_GROUP_READABLE,
    S_IRGRP },
  { "ardfil", MU_FILE_SAFETY_WORLD_READABLE, MU_ERR_PERM_WORLD_READABLE,
    S_IROTH },
  { "gwrfil", MU_FILE_SAFETY_GROUP_WRITABLE, MU_ERR_PERM_GROUP_WRITABLE,
    S_IWGRP },
  { "awrfil", MU_FILE_SAFETY_WORLD_WRITABLE, MU_ERR_PERM_WORLD_WRITABLE,
    S_IWOTH },
  { "linkwrdir", MU_FILE_SAFETY_LINKED_WRDIR,   MU_ERR_PERM_LINKED_WRDIR,
    0, 1, _check_linkwrdir },
  { "gwrdir", MU_FILE_SAFETY_DIR_IWGRP,      MU_ERR_PERM_DIR_IWGRP,
    0, 1, _check_gwrdir },
  { "awrdir", MU_FILE_SAFETY_DIR_IWOTH,      MU_ERR_PERM_DIR_IWOTH,
    0, 1, _check_awrdir },
  { 0 }
};

struct file_id
{
  dev_t dev;
  ino_t inode;
};

static int
file_id_cmp (const void *item, const void *data)
{
  const struct file_id *a = item;
  const struct file_id *b = data;

  if (a->dev != b->dev)
    return 1;
  if (a->inode != b->inode)
    return 1;
  return 0;
}

static int
file_id_lookup (mu_list_t idlist, dev_t dev, ino_t ino)
{
  struct file_id id;

  id.dev = dev;
  id.inode = ino;
  return mu_list_locate (idlist, &id, NULL);
}

static int
file_id_remember (mu_list_t idlist, dev_t dev, ino_t ino)
{
  struct file_id *id = malloc (sizeof (*id));
  if (!id)
    {
      mu_error ("%s", mu_strerror (errno));
      return 1;
    }
  id->dev = dev;
  id->inode = ino;
  return mu_list_append (idlist, id);
}

static struct safety_checker *
_find_safety_checker (const char *name)
{
  struct safety_checker *pck;
  for (pck = file_safety_check_tab; pck->flag; pck++)
    if (strcmp (pck->name, name) == 0)
      return pck;
  return NULL;
}

const char *
mu_file_safety_code_to_name (int code)
{
  struct safety_checker *pck;
  for (pck = file_safety_check_tab; pck->flag; pck++)
    {
      if (pck->flag == code)
	return pck->name;
    }
  return NULL;
}

int
mu_file_safety_name_to_code (const char *name, int *pcode)
{
  struct safety_checker *pck = _find_safety_checker (name);
  if (pck)
    {
      *pcode = pck->flag;
      return 0;
    }
  return MU_ERR_NOENT;
}

int
mu_file_safety_name_to_error (const char *name, int *pcode)
{
  struct safety_checker *pck = _find_safety_checker (name);
  if (pck)
    {
      *pcode = pck->err;
      return 0;
    }
  return MU_ERR_NOENT;
}

int
mu_file_safety_check (const char *filename, int mode, uid_t uid,
		      mu_list_t idlist)
{
  struct file_check_buffer buf;
  struct safety_checker *pck;

  if (!filename)
    return EFAULT;
  memset (&buf, 0, sizeof (buf));
  if (lstat (filename, &buf.filst) != 0)
    return errno;
  if ((buf.islink = S_ISLNK (buf.filst.st_mode)) != 0)
    {
      if (stat (filename, &buf.filst) != 0)
	return errno;
    }

  if (idlist)
    {
      mu_list_set_destroy_item (idlist, mu_list_free_item);
      mu_list_set_comparator (idlist, file_id_cmp);
      if (file_id_lookup (idlist, buf.filst.st_dev, buf.filst.st_ino) == 0)
	return MU_ERR_EXISTS;
    }

  if ((mode & MU_FILE_SAFETY_OWNER_MISMATCH) && uid != buf.filst.st_uid)
    return MU_ERR_PERM_OWNER_MISMATCH;

  for (pck = file_safety_check_tab; pck->flag; pck++)
    if (mode & pck->flag)
      {
	if (pck->cdir && !buf.cdir)
	  {
	    char *dirname, *p;
	    int rc;

	    p = strrchr (filename, '/');
	    if (!p)
	      dirname = strdup (".");
	    else if (p == filename)
	      dirname = strdup ("/");
	    else
	      {
		size_t len = p - filename;
		dirname = malloc (len + 1);
		if (dirname)
		  {
		    memcpy (dirname, filename, len);
		    dirname[len] = 0;
		  }
	      }
	    if (!dirname)
	      return ENOMEM;
	    rc = stat (dirname, &buf.dirst);
	    free (dirname);
	    if (rc)
	      return errno;
	    buf.cdir = 1;
	  }
	if ((pck->fun && pck->fun (&buf)) ||
	    (buf.filst.st_mode & pck->mode))
	  return pck->err;
      }
  if (idlist)
    file_id_remember (idlist, buf.filst.st_dev, buf.filst.st_ino);
  return 0;
}

int
mu_file_mode_to_safety_criteria (int mode)
{
  int fl = 0;
  struct safety_checker *pck;

  for (pck = file_safety_check_tab; pck->name; pck++)
    if (!(mode & pck->mode))
      fl |= pck->flag;
  return fl;
}

int
mu_safety_criteria_to_file_mode (int crit)
{
  int mode = 0666;
  struct safety_checker *pck;

  for (pck = file_safety_check_tab; pck->name; pck++)
    if (pck->flag && (crit & pck->flag))
      mode &= ~pck->mode;
  return mode;
}
