/* fhandler_registry.cc: fhandler for /proc/registry virtual filesystem

   Copyright 2002, 2003, 2003, 2004 Red Hat, Inc.

This file is part of Cygwin.

This software is a copyrighted work licensed under the terms of the
Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
details. */

/* FIXME: Access permissions are ignored at the moment.  */

#include "winsup.h"
#include <unistd.h>
#include <stdlib.h>
#include <sys/cygwin.h>
#include "cygerrno.h"
#include "security.h"
#include "path.h"
#include "fhandler.h"
#include "dtable.h"
#include "cygheap.h"
#include <assert.h>

#define _COMPILING_NEWLIB
#include <dirent.h>

static const int registry_len = sizeof ("registry") - 1;
/* If this bit is set in __d_position then we are enumerating values,
 * else sub-keys. keeping track of where we are is horribly messy
 * the bottom 16 bits are the absolute position and the top 15 bits
 * make up the value index if we are enuerating values.
 */
static const _off_t REG_ENUM_VALUES_MASK = 0x8000000;
static const _off_t REG_POSITION_MASK = 0xffff;

/* List of root keys in /proc/registry.
 * Possibly we should filter out those not relevant to the flavour of Windows
 * Cygwin is running on.
 */
static const char *registry_listing[] =
{
  ".",
  "..",
  "HKEY_CLASSES_ROOT",
  "HKEY_CURRENT_CONFIG",
  "HKEY_CURRENT_USER",
  "HKEY_LOCAL_MACHINE",
  "HKEY_USERS",
  "HKEY_DYN_DATA",		// 95/98/Me
  "HKEY_PERFORMANCE_DATA",	// NT/2000/XP
  NULL
};

static const HKEY registry_keys[] =
{
  (HKEY) INVALID_HANDLE_VALUE,
  (HKEY) INVALID_HANDLE_VALUE,
  HKEY_CLASSES_ROOT,
  HKEY_CURRENT_CONFIG,
  HKEY_CURRENT_USER,
  HKEY_LOCAL_MACHINE,
  HKEY_USERS,
  HKEY_DYN_DATA,
  HKEY_PERFORMANCE_DATA
};

static const int ROOT_KEY_COUNT = sizeof (registry_keys) / sizeof (HKEY);

/* These get added to each subdirectory in /proc/registry.
 * If we wanted to implement writing, we could maybe add a '.writable' entry or
 * suchlike.
 */
static const char *special_dot_files[] =
{
  ".",
  "..",
  NULL
};

static const int SPECIAL_DOT_FILE_COUNT =
  (sizeof (special_dot_files) / sizeof (const char *)) - 1;

/* Name given to default values */
static const char *DEFAULT_VALUE_NAME = "@";

static HKEY open_key (const char *name, REGSAM access, bool isValue);

/* Returns 0 if path doesn't exist, >0 if path is a directory,
 * <0 if path is a file.
 *
 * We open the last key but one and then enum it's sub-keys and values to see if the
 * final component is there. This gets round the problem of not having security access
 * to the final key in the path.
 */
int
fhandler_registry::exists ()
{
  int file_type = 0, index = 0, pathlen;
  DWORD buf_size = CYG_MAX_PATH;
  LONG error;
  char buf[buf_size];
  const char *file;
  HKEY hKey = (HKEY) INVALID_HANDLE_VALUE;

  const char *path = get_name ();
  debug_printf ("exists (%s)", path);
  path += proc_len + registry_len + 1;
  if (*path)
    path++;
  else
    {
      file_type = 2;
      goto out;
    }
  pathlen = strlen (path);
  file = path + pathlen - 1;
  if (isdirsep (*file) && pathlen > 1)
    file--;
  while (!isdirsep (*file))
    file--;
  file++;

  if (file == path)
    {
      for (int i = 0; registry_listing[i]; i++)
	if (path_prefix_p
	    (registry_listing[i], path, strlen (registry_listing[i])))
	  {
	    file_type = 1;
	    goto out;
	  }
      goto out;
    }

  hKey = open_key (path, KEY_READ, false);
  if (hKey != (HKEY) INVALID_HANDLE_VALUE)
    file_type = 1;
  else
    {
      hKey = open_key (path, KEY_READ, true);
      if (hKey == (HKEY) INVALID_HANDLE_VALUE)
	return 0;

      while (ERROR_SUCCESS ==
	     (error = RegEnumKeyEx (hKey, index++, buf, &buf_size, NULL, NULL,
				    NULL, NULL))
	     || (error == ERROR_MORE_DATA))
	{
	  if (pathmatch (buf, file))
	    {
	      file_type = 1;
	      goto out;
	    }
	  buf_size = CYG_MAX_PATH;
	}
      if (error != ERROR_NO_MORE_ITEMS)
	{
	  seterrno_from_win_error (__FILE__, __LINE__, error);
	  goto out;
	}
      index = 0;
      buf_size = CYG_MAX_PATH;
      while (ERROR_SUCCESS ==
	     (error = RegEnumValue (hKey, index++, buf, &buf_size, NULL, NULL,
				    NULL, NULL))
	     || (error == ERROR_MORE_DATA))
	{
	  if (pathmatch (buf, file) || (buf[0] == '\0' &&
					pathmatch (file, DEFAULT_VALUE_NAME)))
	    {
	      file_type = -1;
	      goto out;
	    }
	  buf_size = CYG_MAX_PATH;
	}
      if (error != ERROR_NO_MORE_ITEMS)
	{
	  seterrno_from_win_error (__FILE__, __LINE__, error);
	  goto out;
	}
    }
out:
  if (hKey != (HKEY) INVALID_HANDLE_VALUE)
    RegCloseKey (hKey);
  return file_type;
}

fhandler_registry::fhandler_registry ():
fhandler_proc ()
{
}

int
fhandler_registry::fstat (struct __stat64 *buf)
{
  fhandler_base::fstat (buf);
  buf->st_mode &= ~_IFMT & NO_W;
  int file_type = exists ();
  switch (file_type)
    {
    case 0:
      set_errno (ENOENT);
      return -1;
    case 1:
      buf->st_mode |= S_IFDIR | S_IXUSR | S_IXGRP | S_IXOTH;
      break;
    case 2:
      buf->st_mode |= S_IFDIR | S_IXUSR | S_IXGRP | S_IXOTH;
      buf->st_nlink = ROOT_KEY_COUNT;
      break;
    default:
    case -1:
      buf->st_mode |= S_IFREG;
      buf->st_mode &= NO_X;
      break;
    }
  if (file_type != 0 && file_type != 2)
    {
      HKEY hKey;
      const char *path = get_name () + proc_len + registry_len + 2;
      hKey =
	open_key (path, STANDARD_RIGHTS_READ | KEY_QUERY_VALUE,
		  (file_type < 0) ? true : false);

      if (hKey != (HKEY) INVALID_HANDLE_VALUE)
	{
	  FILETIME ftLastWriteTime;
	  DWORD subkey_count;
	  if (ERROR_SUCCESS ==
	      RegQueryInfoKey (hKey, NULL, NULL, NULL, &subkey_count, NULL,
			       NULL, NULL, NULL, NULL, NULL,
			       &ftLastWriteTime))
	    {
	      to_timestruc_t (&ftLastWriteTime, &buf->st_mtim);
	      buf->st_ctim = buf->st_mtim;
	      time_as_timestruc_t (&buf->st_atim);
	      if (file_type > 0)
		buf->st_nlink = subkey_count;
	      else
		{
		  int pathlen = strlen (path);
		  const char *value_name = path + pathlen - 1;
		  if (isdirsep (*value_name) && pathlen > 1)
		    value_name--;
		  while (!isdirsep (*value_name))
		    value_name--;
		  value_name++;
		  DWORD dwSize;
		  if (ERROR_SUCCESS ==
		      RegQueryValueEx (hKey, value_name, NULL, NULL, NULL,
				       &dwSize))
		    buf->st_size = dwSize;
		}
	      __uid32_t uid;
	      __gid32_t gid;
	      if (get_object_attribute
		  ((HANDLE) hKey, SE_REGISTRY_KEY, &buf->st_mode, &uid,
		   &gid) == 0)
		{
		  buf->st_uid = uid;
		  buf->st_gid = gid;
		  buf->st_mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
		  if (file_type > 0)
		    buf->st_mode |= S_IFDIR;
		  else
		    buf->st_mode &= NO_X;
		}
	    }
	  RegCloseKey (hKey);
	}
    }
  return 0;
}

struct dirent *
fhandler_registry::readdir (DIR * dir)
{
  DWORD buf_size = CYG_MAX_PATH;
  char buf[buf_size];
  HANDLE handle;
  struct dirent *res = NULL;
  const char *path = dir->__d_dirname + proc_len + 1 + registry_len;
  LONG error;

  if (*path == 0)
    {
      if (dir->__d_position >= ROOT_KEY_COUNT)
	goto out;
      strcpy (dir->__d_dirent->d_name, registry_listing[dir->__d_position++]);
      res = dir->__d_dirent;
      goto out;
    }
  if (dir->__handle == INVALID_HANDLE_VALUE
      && dir->__d_position == 0)
    {
      handle = open_key (path + 1, KEY_READ, false);
      dir->__handle = handle;
    }
  if (dir->__handle == INVALID_HANDLE_VALUE)
    goto out;
  if (dir->__d_position < SPECIAL_DOT_FILE_COUNT)
    {
      strcpy (dir->__d_dirent->d_name,
	      special_dot_files[dir->__d_position++]);
      res = dir->__d_dirent;
      goto out;
    }
retry:
  if (dir->__d_position & REG_ENUM_VALUES_MASK)
    /* For the moment, the type of key is ignored here. when write access is added,
     * maybe add an extension for the type of each value?
     */
    error = RegEnumValue ((HKEY) dir->__handle,
			  (dir->__d_position & ~REG_ENUM_VALUES_MASK) >> 16,
			  buf, &buf_size, NULL, NULL, NULL, NULL);
  else
    error =
      RegEnumKeyEx ((HKEY) dir->__handle, dir->__d_position -
		    SPECIAL_DOT_FILE_COUNT, buf, &buf_size, NULL, NULL, NULL,
		    NULL);
  if (error == ERROR_NO_MORE_ITEMS
      && (dir->__d_position & REG_ENUM_VALUES_MASK) == 0)
    {
      /* If we're finished with sub-keys, start on values under this key.  */
      dir->__d_position |= REG_ENUM_VALUES_MASK;
      buf_size = CYG_MAX_PATH;
      goto retry;
    }
  if (error != ERROR_SUCCESS && error != ERROR_MORE_DATA)
    {
      RegCloseKey ((HKEY) dir->__handle);
      dir->__handle = INVALID_HANDLE_VALUE;
      if (error != ERROR_NO_MORE_ITEMS)
	seterrno_from_win_error (__FILE__, __LINE__, error);
      goto out;
    }

  /* We get here if `buf' contains valid data.  */
  if (*buf == 0)
    strcpy (dir->__d_dirent->d_name, DEFAULT_VALUE_NAME);
  else
    strcpy (dir->__d_dirent->d_name, buf);

  dir->__d_position++;
  if (dir->__d_position & REG_ENUM_VALUES_MASK)
    dir->__d_position += 0x10000;
  res = dir->__d_dirent;
out:
  syscall_printf ("%p = readdir (%p)", &dir->__d_dirent, dir);
  return res;
}

_off64_t
fhandler_registry::telldir (DIR * dir)
{
  return dir->__d_position & REG_POSITION_MASK;
}

void
fhandler_registry::seekdir (DIR * dir, _off64_t loc)
{
  /* Unfortunately cannot simply set __d_position due to transition from sub-keys to
   * values.
   */
  rewinddir (dir);
  while (loc > (dir->__d_position & REG_POSITION_MASK))
    if (!readdir (dir))
      break;
}

void
fhandler_registry::rewinddir (DIR * dir)
{
  if (dir->__handle != INVALID_HANDLE_VALUE)
    {
      (void) RegCloseKey ((HKEY) dir->__handle);
      dir->__handle = INVALID_HANDLE_VALUE;
    }
  dir->__d_position = 0;
  return;
}

int
fhandler_registry::closedir (DIR * dir)
{
  int res = 0;
  if (dir->__handle != INVALID_HANDLE_VALUE &&
      RegCloseKey ((HKEY) dir->__handle) != ERROR_SUCCESS)
    {
      __seterrno ();
      res = -1;
    }
  syscall_printf ("%d = closedir (%p)", res, dir);
  return 0;
}

int
fhandler_registry::open (int flags, mode_t mode)
{
  int pathlen;
  const char *file;
  HKEY handle;

  int res = fhandler_virtual::open (flags, mode);
  if (!res)
    goto out;

  const char *path;
  path = get_name () + proc_len + 1 + registry_len;
  if (!*path)
    {
      if ((flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL))
	{
	  set_errno (EEXIST);
	  res = 0;
	  goto out;
	}
      else if (flags & O_WRONLY)
	{
	  set_errno (EISDIR);
	  res = 0;
	  goto out;
	}
      else
	{
	  flags |= O_DIROPEN;
	  goto success;
	}
    }
  path++;
  pathlen = strlen (path);
  file = path + pathlen - 1;
  if (isdirsep (*file) && pathlen > 1)
    file--;
  while (!isdirsep (*file))
    file--;
  file++;

  if (file == path)
    {
      for (int i = 0; registry_listing[i]; i++)
	if (path_prefix_p
	    (registry_listing[i], path, strlen (registry_listing[i])))
	  {
	    if ((flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL))
	      {
		set_errno (EEXIST);
		res = 0;
		goto out;
	      }
	    else if (flags & O_WRONLY)
	      {
		set_errno (EISDIR);
		res = 0;
		goto out;
	      }
	    else
	      {
		flags |= O_DIROPEN;
		goto success;
	      }
	  }

      if (flags & O_CREAT)
	{
	  set_errno (EROFS);
	  res = 0;
	  goto out;
	}
      else
	{
	  set_errno (ENOENT);
	  res = 0;
	  goto out;
	}
    }

  if (flags & O_WRONLY)
    {
      set_errno (EROFS);
      res = 0;
      goto out;
    }

  handle = open_key (path, KEY_READ, true);
  if (handle == (HKEY) INVALID_HANDLE_VALUE)
    {
      res = 0;
      goto out;
    }

  set_io_handle (handle);

  if (pathmatch (file, DEFAULT_VALUE_NAME))
    value_name = cstrdup ("");
  else
    value_name = cstrdup (file);

  if (!fill_filebuf ())
    {
      RegCloseKey (handle);
      res = 0;
      goto out;
    }

  if (flags & O_APPEND)
    position = filesize;
  else
    position = 0;

success:
  res = 1;
  set_flags ((flags & ~O_TEXT) | O_BINARY);
  set_open_status ();
out:
  syscall_printf ("%d = fhandler_registry::open (%p, %d)", res, flags, mode);
  return res;
}

int
fhandler_registry::close ()
{
  int res = fhandler_virtual::close ();
  if (res != 0)
    return res;
  HKEY handle = (HKEY) get_handle ();
  if (handle != (HKEY) INVALID_HANDLE_VALUE)
    {
      if (RegCloseKey (handle) != ERROR_SUCCESS)
	{
	  __seterrno ();
	  res = -1;
	}
    }
  if (value_name)
    cfree (value_name);
  return res;
}

bool
fhandler_registry::fill_filebuf ()
{
  DWORD type, size;
  LONG error;
  HKEY handle = (HKEY) get_handle ();
  if (handle != HKEY_PERFORMANCE_DATA)
    {
      error = RegQueryValueEx (handle, value_name, NULL, &type, NULL, &size);
      if (error != ERROR_SUCCESS)
	{
	  if (error != ERROR_FILE_NOT_FOUND)
	    {
	      seterrno_from_win_error (__FILE__, __LINE__, error);
	      return false;
	    }
	  goto value_not_found;
	}
      bufalloc = size;
      filebuf = (char *) malloc (bufalloc);
      error =
	RegQueryValueEx (handle, value_name, NULL, NULL, (BYTE *) filebuf,
			 &size);
      if (error != ERROR_SUCCESS)
	{
	  seterrno_from_win_error (__FILE__, __LINE__, error);
	  return true;
	}
      filesize = size;
    }
  else
    {
      bufalloc = 0;
      do
	{
	  bufalloc += 1000;
	  filebuf = (char *) realloc (filebuf, bufalloc);
	  size = bufalloc;
	  error = RegQueryValueEx (handle, value_name, NULL, &type,
				   (BYTE *) filebuf, &size);
	  if (error != ERROR_SUCCESS && error != ERROR_MORE_DATA)
	    {
	      if (error != ERROR_FILE_NOT_FOUND)
		{
		  seterrno_from_win_error (__FILE__, __LINE__, error);
		  return true;
		}
	      goto value_not_found;
	    }
	}
      while (error == ERROR_MORE_DATA);
      filesize = size;
    }
  return true;
value_not_found:
  DWORD buf_size = CYG_MAX_PATH;
  char buf[buf_size];
  int index = 0;
  while (ERROR_SUCCESS ==
	 (error = RegEnumKeyEx (handle, index++, buf, &buf_size, NULL, NULL,
				NULL, NULL)) || (error == ERROR_MORE_DATA))
    {
      if (pathmatch (buf, value_name))
	{
	  set_errno (EISDIR);
	  return false;
	}
      buf_size = CYG_MAX_PATH;
    }
  if (error != ERROR_NO_MORE_ITEMS)
    {
      seterrno_from_win_error (__FILE__, __LINE__, error);
      return false;
    }
  set_errno (ENOENT);
  return false;
}

/* Auxillary member function to open registry keys.  */
static HKEY
open_key (const char *name, REGSAM access, bool isValue)
{
  HKEY hKey = (HKEY) INVALID_HANDLE_VALUE;
  HKEY hParentKey = (HKEY) INVALID_HANDLE_VALUE;
  bool parentOpened = false;
  char component[CYG_MAX_PATH];

  while (*name)
    {
      const char *anchor = name;
      while (*name && !isdirsep (*name))
	name++;
      strncpy (component, anchor, name - anchor);
      component[name - anchor] = '\0';
      if (*name)
	name++;
      if (*name == 0 && isValue == true)
	goto out;

      if (hParentKey != (HKEY) INVALID_HANDLE_VALUE)
	{
	  REGSAM effective_access = KEY_READ;
	  if ((strchr (name, '/') == NULL && isValue == true) || *name == 0)
	    effective_access = access;
	  LONG
	    error =
	    RegOpenKeyEx (hParentKey, component, 0, effective_access, &hKey);
	  if (error != ERROR_SUCCESS)
	    {
	      hKey = (HKEY) INVALID_HANDLE_VALUE;
	      seterrno_from_win_error (__FILE__, __LINE__, error);
	      return hKey;
	    }
	  if (parentOpened)
	    RegCloseKey (hParentKey);
	  hParentKey = hKey;
	  parentOpened = true;
	}
      else
	{
	  for (int i = 0; registry_listing[i]; i++)
	    if (pathmatch (component, registry_listing[i]))
	      hKey = registry_keys[i];
	  if (hKey == (HKEY) INVALID_HANDLE_VALUE)
	    return hKey;
	  hParentKey = hKey;
	}
    }
out:
  return hKey;
}
