//
// Automated Testing Framework (atf)
//
// Copyright (c) 2007 The NetBSD Foundation, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND
// CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

extern "C" {
#include <sys/param.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>

#include <dirent.h>
#include <libgen.h>
#include <unistd.h>
}

#include <cassert>
#include <cerrno>
#include <cstdlib>
#include <cstring>

#include "auto_array.hpp"
#include "env.hpp"
#include "exceptions.hpp"
#include "fs.hpp"
#include "text.hpp"
#include "user.hpp"

namespace impl = tools::fs;
#define IMPL_NAME "tools::fs"

// ------------------------------------------------------------------------
// Auxiliary functions.
// ------------------------------------------------------------------------

static void cleanup_aux(const impl::path&, dev_t, bool);
static void cleanup_aux_dir(const impl::path&, const impl::file_info&,
                           bool);
static void do_unmount(const impl::path&);
static bool safe_access(const impl::path&, int, int);

static const int access_f = 1 << 0;
static const int access_r = 1 << 1;
static const int access_w = 1 << 2;
static const int access_x = 1 << 3;

//!
//! An implementation of access(2) but using the effective user value
//! instead of the real one.  Also avoids false positives for root when
//! asking for execute permissions, which appear in SunOS.
//!
static
void
eaccess(const tools::fs::path& p, int mode)
{
   assert(mode & access_f || mode & access_r ||
          mode & access_w || mode & access_x);

   struct stat st;
   if (lstat(p.c_str(), &st) == -1)
       throw tools::system_error(IMPL_NAME "::eaccess",
                                 "Cannot get information from file " +
                                 p.str(), errno);

   /* Early return if we are only checking for existence and the file
    * exists (stat call returned). */
   if (mode & access_f)
       return;

   bool ok = false;
   if (tools::user::is_root()) {
       if (!ok && !(mode & access_x)) {
           /* Allow root to read/write any file. */
           ok = true;
       }

       if (!ok && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
           /* Allow root to execute the file if any of its execution bits
            * are set. */
           ok = true;
       }
   } else {
       if (!ok && (tools::user::euid() == st.st_uid)) {
           ok = ((mode & access_r) && (st.st_mode & S_IRUSR)) ||
                ((mode & access_w) && (st.st_mode & S_IWUSR)) ||
                ((mode & access_x) && (st.st_mode & S_IXUSR));
       }
       if (!ok && tools::user::is_member_of_group(st.st_gid)) {
           ok = ((mode & access_r) && (st.st_mode & S_IRGRP)) ||
                ((mode & access_w) && (st.st_mode & S_IWGRP)) ||
                ((mode & access_x) && (st.st_mode & S_IXGRP));
       }
       if (!ok && ((tools::user::euid() != st.st_uid) &&
                   !tools::user::is_member_of_group(st.st_gid))) {
           ok = ((mode & access_r) && (st.st_mode & S_IROTH)) ||
                ((mode & access_w) && (st.st_mode & S_IWOTH)) ||
                ((mode & access_x) && (st.st_mode & S_IXOTH));
       }
   }

   if (!ok)
       throw tools::system_error(IMPL_NAME "::eaccess", "Access check failed",
                                 EACCES);
}

//!
//! \brief A controlled version of access(2).
//!
//! This function reimplements the standard access(2) system call to
//! safely control its exit status and raise an exception in case of
//! failure.
//!
static
bool
safe_access(const impl::path& p, int mode, int experr)
{
   try {
       eaccess(p, mode);
       return true;
   } catch (const tools::system_error& e) {
       if (e.code() == experr)
           return false;
       else
           throw e;
   }
}

// The cleanup routines below are tricky: they are executed immediately after
// a test case's death, and after we have forcibly killed any stale processes.
// However, even if the processes are dead, this does not mean that the file
// system we are scanning is stable.  In particular, if the test case has
// mounted file systems through fuse/puffs, the fact that the processes died
// does not mean that the file system is truly unmounted.
//
// The code below attempts to cope with this by catching errors and either
// ignoring them or retrying the actions on the same file/directory a few times
// before giving up.
static const int max_retries = 5;
static const int retry_delay_in_seconds = 1;

// The erase parameter in this routine is to control nested mount points.
// We want to descend into a mount point to unmount anything that is
// mounted under it, but we do not want to delete any files while doing
// this traversal.  In other words, we erase files until we cross the
// first mount point, and after that point we only scan and unmount.
static
void
cleanup_aux(const impl::path& p, dev_t parent_device, bool erase)
{
   try {
       impl::file_info fi(p);

       if (fi.get_type() == impl::file_info::dir_type)
           cleanup_aux_dir(p, fi, fi.get_device() == parent_device);

       if (fi.get_device() != parent_device)
           do_unmount(p);

       if (erase) {
           if (fi.get_type() == impl::file_info::dir_type)
               impl::rmdir(p);
           else
               impl::remove(p);
       }
   } catch (const tools::system_error& e) {
       if (e.code() != ENOENT && e.code() != ENOTDIR)
           throw e;
   }
}

static
void
cleanup_aux_dir(const impl::path& p, const impl::file_info& fi,
               bool erase)
{
   if (erase && ((fi.get_mode() & S_IRWXU) != S_IRWXU)) {
       int retries = max_retries;
retry_chmod:
       if (chmod(p.c_str(), fi.get_mode() | S_IRWXU) == -1) {
           if (retries > 0) {
               retries--;
               ::sleep(retry_delay_in_seconds);
               goto retry_chmod;
           } else {
               throw tools::system_error(IMPL_NAME "::cleanup(" +
                                       p.str() + ")", "chmod(2) failed",
                                       errno);
           }
       }
   }

   std::set< std::string > subdirs;
   {
       bool ok = false;
       int retries = max_retries;
       while (!ok) {
           assert(retries > 0);
           try {
               const impl::directory d(p);
               subdirs = d.names();
               ok = true;
           } catch (const tools::system_error& e) {
               retries--;
               if (retries == 0)
                   throw e;
               ::sleep(retry_delay_in_seconds);
           }
       }
       assert(ok);
   }

   for (std::set< std::string >::const_iterator iter = subdirs.begin();
        iter != subdirs.end(); iter++) {
       const std::string& name = *iter;
       if (name != "." && name != "..")
           cleanup_aux(p / name, fi.get_device(), erase);
   }
}

static
void
do_unmount(const impl::path& in_path)
{
   // At least, FreeBSD's unmount(2) requires the path to be absolute.
   // Let's make it absolute in all cases just to be safe that this does
   // not affect other systems.
   const impl::path& abs_path = in_path.is_absolute() ?
       in_path : in_path.to_absolute();

   int retries = max_retries;
retry_unmount:
   if (unmount(abs_path.c_str(), 0) == -1) {
       if (errno == EBUSY && retries > 0) {
           retries--;
           ::sleep(retry_delay_in_seconds);
           goto retry_unmount;
       } else {
           throw tools::system_error(IMPL_NAME "::cleanup(" + in_path.str() +
                                   ")", "unmount(2) failed", errno);
       }
   }
}

static
std::string
normalize(const std::string& in)
{
   assert(!in.empty());

   std::string out;

   std::string::size_type pos = 0;
   do {
       const std::string::size_type next_pos = in.find('/', pos);

       const std::string component = in.substr(pos, next_pos - pos);
       if (!component.empty()) {
           if (pos == 0)
               out += component;
           else if (component != ".")
               out += "/" + component;
       }

       if (next_pos == std::string::npos)
           pos = next_pos;
       else
           pos = next_pos + 1;
   } while (pos != std::string::npos);

   return out.empty() ? "/" : out;
}

// ------------------------------------------------------------------------
// The "path" class.
// ------------------------------------------------------------------------

impl::path::path(const std::string& s) :
   m_data(normalize(s))
{
}

impl::path::~path(void)
{
}

const char*
impl::path::c_str(void)
   const
{
   return m_data.c_str();
}

std::string
impl::path::str(void)
   const
{
   return m_data;
}

bool
impl::path::is_absolute(void)
   const
{
   return !m_data.empty() && m_data[0] == '/';
}

bool
impl::path::is_root(void)
   const
{
   return m_data == "/";
}

impl::path
impl::path::branch_path(void)
   const
{
   const std::string::size_type endpos = m_data.rfind('/');
   if (endpos == std::string::npos)
       return path(".");
   else if (endpos == 0)
       return path("/");
   else
       return path(m_data.substr(0, endpos));
}

std::string
impl::path::leaf_name(void)
   const
{
   std::string::size_type begpos = m_data.rfind('/');
   if (begpos == std::string::npos)
       begpos = 0;
   else
       begpos++;

   return m_data.substr(begpos);
}

impl::path
impl::path::to_absolute(void)
   const
{
   assert(!is_absolute());
   return get_current_dir() / m_data;
}

bool
impl::path::operator==(const path& p)
   const
{
   return m_data == p.m_data;
}

bool
impl::path::operator!=(const path& p)
   const
{
   return m_data != p.m_data;
}

impl::path
impl::path::operator/(const std::string& p)
   const
{
   return path(m_data + "/" + normalize(p));
}

impl::path
impl::path::operator/(const path& p)
   const
{
   return path(m_data) / p.m_data;
}

bool
impl::path::operator<(const path& p)
   const
{
   return std::strcmp(m_data.c_str(), p.m_data.c_str()) < 0;
}

// ------------------------------------------------------------------------
// The "file_info" class.
// ------------------------------------------------------------------------

const int impl::file_info::blk_type = 1;
const int impl::file_info::chr_type = 2;
const int impl::file_info::dir_type = 3;
const int impl::file_info::fifo_type = 4;
const int impl::file_info::lnk_type = 5;
const int impl::file_info::reg_type = 6;
const int impl::file_info::sock_type = 7;
const int impl::file_info::wht_type = 8;

impl::file_info::file_info(const path& p)
{
   if (lstat(p.c_str(), &m_sb) == -1)
       throw system_error(IMPL_NAME "::file_info",
                          "Cannot get information of " + p.str() + "; " +
                          "lstat(2) failed", errno);

   int type = m_sb.st_mode & S_IFMT;
   switch (type) {
   case S_IFBLK:  m_type = blk_type;  break;
   case S_IFCHR:  m_type = chr_type;  break;
   case S_IFDIR:  m_type = dir_type;  break;
   case S_IFIFO:  m_type = fifo_type; break;
   case S_IFLNK:  m_type = lnk_type;  break;
   case S_IFREG:  m_type = reg_type;  break;
   case S_IFSOCK: m_type = sock_type; break;
   case S_IFWHT:  m_type = wht_type;  break;
   default:
       throw system_error(IMPL_NAME "::file_info", "Unknown file type "
                          "error", EINVAL);
   }
}

impl::file_info::~file_info(void)
{
}

dev_t
impl::file_info::get_device(void)
   const
{
   return m_sb.st_dev;
}

ino_t
impl::file_info::get_inode(void)
   const
{
   return m_sb.st_ino;
}

mode_t
impl::file_info::get_mode(void)
   const
{
   return m_sb.st_mode & ~S_IFMT;
}

off_t
impl::file_info::get_size(void)
   const
{
   return m_sb.st_size;
}

int
impl::file_info::get_type(void)
   const
{
   return m_type;
}

bool
impl::file_info::is_owner_readable(void)
   const
{
   return m_sb.st_mode & S_IRUSR;
}

bool
impl::file_info::is_owner_writable(void)
   const
{
   return m_sb.st_mode & S_IWUSR;
}

bool
impl::file_info::is_owner_executable(void)
   const
{
   return m_sb.st_mode & S_IXUSR;
}

bool
impl::file_info::is_group_readable(void)
   const
{
   return m_sb.st_mode & S_IRGRP;
}

bool
impl::file_info::is_group_writable(void)
   const
{
   return m_sb.st_mode & S_IWGRP;
}

bool
impl::file_info::is_group_executable(void)
   const
{
   return m_sb.st_mode & S_IXGRP;
}

bool
impl::file_info::is_other_readable(void)
   const
{
   return m_sb.st_mode & S_IROTH;
}

bool
impl::file_info::is_other_writable(void)
   const
{
   return m_sb.st_mode & S_IWOTH;
}

bool
impl::file_info::is_other_executable(void)
   const
{
   return m_sb.st_mode & S_IXOTH;
}

// ------------------------------------------------------------------------
// The "directory" class.
// ------------------------------------------------------------------------

impl::directory::directory(const path& p)
{
   DIR* dp = ::opendir(p.c_str());
   if (dp == NULL)
       throw system_error(IMPL_NAME "::directory::directory(" +
                          p.str() + ")", "opendir(3) failed", errno);

   struct dirent* dep;
   while ((dep = ::readdir(dp)) != NULL) {
       path entryp = p / dep->d_name;
       insert(value_type(dep->d_name, file_info(entryp)));
   }

   if (::closedir(dp) == -1)
       throw system_error(IMPL_NAME "::directory::directory(" +
                          p.str() + ")", "closedir(3) failed", errno);
}

std::set< std::string >
impl::directory::names(void)
   const
{
   std::set< std::string > ns;

   for (const_iterator iter = begin(); iter != end(); iter++)
       ns.insert((*iter).first);

   return ns;
}

// ------------------------------------------------------------------------
// The "temp_dir" class.
// ------------------------------------------------------------------------

impl::temp_dir::temp_dir(const path& p)
{
   tools::auto_array< char > buf(new char[p.str().length() + 1]);
   std::strcpy(buf.get(), p.c_str());
   if (::mkdtemp(buf.get()) == NULL)
       throw tools::system_error(IMPL_NAME "::temp_dir::temp_dir(" +
                               p.str() + ")", "mkdtemp(3) failed",
                               errno);

   m_path.reset(new path(buf.get()));
}

impl::temp_dir::~temp_dir(void)
{
   cleanup(*m_path);
}

const impl::path&
impl::temp_dir::get_path(void)
   const
{
   return *m_path;
}

// ------------------------------------------------------------------------
// Free functions.
// ------------------------------------------------------------------------

bool
impl::exists(const path& p)
{
   try {
       eaccess(p, access_f);
       return true;
   } catch (const system_error& e) {
       if (e.code() == ENOENT)
           return false;
       else
           throw;
   }
}

bool
impl::have_prog_in_path(const std::string& prog)
{
   assert(prog.find('/') == std::string::npos);

   // Do not bother to provide a default value for PATH.  If it is not
   // there something is broken in the user's environment.
   if (!tools::env::has("PATH"))
       throw std::runtime_error("PATH not defined in the environment");
   std::vector< std::string > dirs =
       tools::text::split(tools::env::get("PATH"), ":");

   bool found = false;
   for (std::vector< std::string >::const_iterator iter = dirs.begin();
        !found && iter != dirs.end(); iter++) {
       const path& dir = path(*iter);

       if (is_executable(dir / prog))
           found = true;
   }
   return found;
}

bool
impl::is_executable(const path& p)
{
   if (!exists(p))
       return false;
   return safe_access(p, access_x, EACCES);
}

void
impl::remove(const path& p)
{
   if (file_info(p).get_type() == file_info::dir_type)
       throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")",
                                 "Is a directory",
                                 EPERM);
   if (::unlink(p.c_str()) == -1)
       throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")",
                                 "unlink(" + p.str() + ") failed",
                                 errno);
}

void
impl::rmdir(const path& p)
{
   if (::rmdir(p.c_str())) {
       if (errno == EEXIST) {
           /* Some operating systems (e.g. OpenSolaris 200906) return
            * EEXIST instead of ENOTEMPTY for non-empty directories.
            * Homogenize the return value so that callers don't need
            * to bother about differences in operating systems. */
           errno = ENOTEMPTY;
       }
       throw system_error(IMPL_NAME "::rmdir", "Cannot remove directory",
                          errno);
   }
}

void
impl::change_ownership(const path& p, const std::pair < int, int >& user)
{
   if (::chown(p.c_str(), user.first, user.second) == -1) {
       std::stringstream ss;
       ss << IMPL_NAME "::chown(" << p.str() << ", " << user.first << ", "
          << user.second << ")";
       throw tools::system_error(ss.str(), "chown(2) failed", errno);
   }
}

impl::path
impl::change_directory(const path& dir)
{
   path olddir = get_current_dir();

   if (olddir != dir) {
       if (::chdir(dir.c_str()) == -1)
           throw tools::system_error(IMPL_NAME "::chdir(" + dir.str() + ")",
                                   "chdir(2) failed", errno);
   }

   return olddir;
}

void
impl::cleanup(const path& p)
{
   impl::file_info fi(p);
   cleanup_aux(p, fi.get_device(), true);
}

impl::path
impl::get_current_dir(void)
{
   char *cwd = getcwd(NULL, 0);
   if (cwd == NULL)
       throw tools::system_error(IMPL_NAME "::get_current_dir()",
                               "getcwd() failed", errno);

   try {
       impl::path p(cwd);
       free(cwd);
       return p;
   } catch(...) {
       free(cwd);
       throw;
   }
}