/* Work around platform bugs in utime.
   Copyright (C) 2017-2018 Free Software Foundation, Inc.

   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 3 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 <https://www.gnu.org/licenses/>.  */

/* Written by Bruno Haible.  */

#include <config.h>

/* Specification.  */
#include <utime.h>

#if defined _WIN32 && ! defined __CYGWIN__

# include <errno.h>
# include <stdbool.h>
# include <windows.h>
# include "filename.h"
# include "malloca.h"

int
_gl_utimens_windows (const char *name, struct timespec ts[2])
{
  /* POSIX <http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13>
     specifies: "More than two leading <slash> characters shall be treated as
     a single <slash> character."  */
  if (ISSLASH (name[0]) && ISSLASH (name[1]) && ISSLASH (name[2]))
    {
      name += 2;
      while (ISSLASH (name[1]))
        name++;
    }

  size_t len = strlen (name);
  size_t drive_prefix_len = (HAS_DEVICE (name) ? 2 : 0);

  /* Remove trailing slashes (except the very first one, at position
     drive_prefix_len), but remember their presence.  */
  size_t rlen;
  bool check_dir = false;

  rlen = len;
  while (rlen > drive_prefix_len && ISSLASH (name[rlen-1]))
    {
      check_dir = true;
      if (rlen == drive_prefix_len + 1)
        break;
      rlen--;
    }

  const char *rname;
  char *malloca_rname;
  if (rlen == len)
    {
      rname = name;
      malloca_rname = NULL;
    }
  else
    {
      malloca_rname = malloca (rlen + 1);
      if (malloca_rname == NULL)
        {
          errno = ENOMEM;
          return -1;
        }
      memcpy (malloca_rname, name, rlen);
      malloca_rname[rlen] = '\0';
      rname = malloca_rname;
    }

  DWORD error;

  /* Open a handle to the file.
     CreateFile
     <https://msdn.microsoft.com/en-us/library/aa363858.aspx>
     <https://msdn.microsoft.com/en-us/library/aa363874.aspx>  */
  HANDLE handle =
    CreateFile (rname,
                FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                NULL,
                OPEN_EXISTING,
                /* FILE_FLAG_POSIX_SEMANTICS (treat file names that differ only
                   in case as different) makes sense only when applied to *all*
                   filesystem operations.  */
                FILE_FLAG_BACKUP_SEMANTICS /* | FILE_FLAG_POSIX_SEMANTICS */,
                NULL);
  if (handle == INVALID_HANDLE_VALUE)
    {
      error = GetLastError ();
      goto failed;
    }

  if (check_dir)
    {
      /* GetFileAttributes
         <https://msdn.microsoft.com/en-us/library/aa364944.aspx>  */
      DWORD attributes = GetFileAttributes (rname);
      if (attributes == INVALID_FILE_ATTRIBUTES)
        {
          error = GetLastError ();
          CloseHandle (handle);
          goto failed;
        }
      if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
        {
          CloseHandle (handle);
          if (malloca_rname != NULL)
            freea (malloca_rname);
          errno = ENOTDIR;
          return -1;
        }
    }

  {
    /* Use SetFileTime(). See
       <https://msdn.microsoft.com/en-us/library/ms724933.aspx>
       <https://msdn.microsoft.com/en-us/library/ms724284.aspx>  */
    FILETIME last_access_time;
    FILETIME last_write_time;
    if (ts == NULL)
      {
        /* GetSystemTimeAsFileTime is the same as
           GetSystemTime followed by SystemTimeToFileTime.
           <https://msdn.microsoft.com/en-us/library/ms724397.aspx>.
           It would be overkill to use
           GetSystemTimePreciseAsFileTime
           <https://msdn.microsoft.com/en-us/library/hh706895.aspx>.  */
        FILETIME current_time;
        GetSystemTimeAsFileTime (&current_time);
        last_access_time = current_time;
        last_write_time = current_time;
      }
    else
      {
        {
          ULONGLONG time_since_16010101 =
            (ULONGLONG) ts[0].tv_sec * 10000000 + ts[0].tv_nsec / 100 + 116444736000000000LL;
          last_access_time.dwLowDateTime = (DWORD) time_since_16010101;
          last_access_time.dwHighDateTime = time_since_16010101 >> 32;
        }
        {
          ULONGLONG time_since_16010101 =
            (ULONGLONG) ts[1].tv_sec * 10000000 + ts[1].tv_nsec / 100 + 116444736000000000LL;
          last_write_time.dwLowDateTime = (DWORD) time_since_16010101;
          last_write_time.dwHighDateTime = time_since_16010101 >> 32;
        }
      }
    if (SetFileTime (handle, NULL, &last_access_time, &last_write_time))
      {
        CloseHandle (handle);
        if (malloca_rname != NULL)
          freea (malloca_rname);
        return 0;
      }
    else
      {
        #if 0
        DWORD sft_error = GetLastError ();
        fprintf (stderr, "utimens SetFileTime error 0x%x\n", (unsigned int) sft_error);
        #endif
        CloseHandle (handle);
        if (malloca_rname != NULL)
          freea (malloca_rname);
        errno = EINVAL;
        return -1;
      }
  }

 failed:
  {
    #if 0
    fprintf (stderr, "utimens CreateFile/GetFileAttributes error 0x%x\n", (unsigned int) error);
    #endif
    if (malloca_rname != NULL)
      freea (malloca_rname);

    switch (error)
      {
      /* Some of these errors probably cannot happen with the specific flags
         that we pass to CreateFile.  But who knows...  */
      case ERROR_FILE_NOT_FOUND: /* The last component of rname does not exist.  */
      case ERROR_PATH_NOT_FOUND: /* Some directory component in rname does not exist.  */
      case ERROR_BAD_PATHNAME:   /* rname is such as '\\server'.  */
      case ERROR_BAD_NETPATH:    /* rname is such as '\\nonexistentserver\share'.  */
      case ERROR_BAD_NET_NAME:   /* rname is such as '\\server\nonexistentshare'.  */
      case ERROR_INVALID_NAME:   /* rname contains wildcards, misplaced colon, etc.  */
      case ERROR_DIRECTORY:
        errno = ENOENT;
        break;

      case ERROR_ACCESS_DENIED:  /* rname is such as 'C:\System Volume Information\foo'.  */
      case ERROR_SHARING_VIOLATION: /* rname is such as 'C:\pagefile.sys'.  */
        errno = (ts != NULL ? EPERM : EACCES);
        break;

      case ERROR_OUTOFMEMORY:
        errno = ENOMEM;
        break;

      case ERROR_WRITE_PROTECT:
        errno = EROFS;
        break;

      case ERROR_WRITE_FAULT:
      case ERROR_READ_FAULT:
      case ERROR_GEN_FAILURE:
        errno = EIO;
        break;

      case ERROR_BUFFER_OVERFLOW:
      case ERROR_FILENAME_EXCED_RANGE:
        errno = ENAMETOOLONG;
        break;

      case ERROR_DELETE_PENDING: /* XXX map to EACCESS or EPERM? */
        errno = EPERM;
        break;

      default:
        errno = EINVAL;
        break;
      }

    return -1;
  }
}

int
utime (const char *name, const struct utimbuf *ts)
{
  if (ts == NULL)
    return _gl_utimens_windows (name, NULL);
  else
    {
      struct timespec ts_with_nanoseconds[2];
      ts_with_nanoseconds[0].tv_sec = ts->actime;
      ts_with_nanoseconds[0].tv_nsec = 0;
      ts_with_nanoseconds[1].tv_sec = ts->modtime;
      ts_with_nanoseconds[1].tv_nsec = 0;
      return _gl_utimens_windows (name, ts_with_nanoseconds);
    }
}

#endif
