// Class filesystem::directory_entry etc. -*- C++ -*-

// Copyright (C) 2014-2023 Free Software Foundation, Inc.
//
// This file is part of the GNU ISO C++ Library.  This library 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, 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 General Public License for more details.

// Under Section 7 of GPL version 3, you are granted additional
// permissions described in the GCC Runtime Library Exception, version
// 3.1, as published by the Free Software Foundation.

// You should have received a copy of the GNU General Public License and
// a copy of the GCC Runtime Library Exception along with this program;
// see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
// <http://www.gnu.org/licenses/>.

#ifndef _GLIBCXX_USE_CXX11_ABI
# define _GLIBCXX_USE_CXX11_ABI 1
#endif

#include <bits/largefile-config.h>
#include <filesystem>
#include <utility>
#include <stack>
#include <string.h>
#include <errno.h>
#define _GLIBCXX_BEGIN_NAMESPACE_FILESYSTEM namespace filesystem {
#define _GLIBCXX_END_NAMESPACE_FILESYSTEM }
#include "../filesystem/dir-common.h"

namespace fs = std::filesystem;
namespace posix = std::filesystem::__gnu_posix;

template class std::__shared_ptr<fs::_Dir>;
template class std::__shared_ptr<fs::recursive_directory_iterator::_Dir_stack>;

struct fs::_Dir : _Dir_base
{
  _Dir(const fs::path& p, bool skip_permission_denied, bool nofollow,
       [[maybe_unused]] bool filename_only, error_code& ec)
  : _Dir_base(p.c_str(), skip_permission_denied, nofollow, ec)
  {
#if _GLIBCXX_HAVE_DIRFD && _GLIBCXX_HAVE_OPENAT && _GLIBCXX_HAVE_UNLINKAT
    if (filename_only)
      return; // Do not store path p when we aren't going to use it.
#endif

    if (!ec)
      path = p;
  }

  _Dir(_Dir_base&& d, const path& p) : _Dir_base(std::move(d)), path(p) { }

  _Dir(_Dir&&) = default;

  // Returns false when the end of the directory entries is reached.
  // Reports errors by setting ec.
  bool advance(bool skip_permission_denied, error_code& ec) noexcept
  {
    if (const auto entp = _Dir_base::advance(skip_permission_denied, ec))
      {
	auto name = path;
	name /= entp->d_name;
	file_type type = file_type::none;
#ifdef _GLIBCXX_HAVE_STRUCT_DIRENT_D_TYPE
	// Even if the OS supports dirent::d_type the filesystem might not:
	if (entp->d_type != DT_UNKNOWN)
	  type = get_file_type(*entp);
#endif
	entry = fs::directory_entry{std::move(name), type};
	return true;
      }
    else if (!ec)
      {
	// reached the end
	entry = {};
      }
    return false;
  }

  bool advance(error_code& ec) noexcept { return advance(false, ec); }

  // Returns false when the end of the directory entries is reached.
  // Reports errors by throwing.
  bool advance(bool skip_permission_denied = false)
  {
    error_code ec;
    const bool ok = advance(skip_permission_denied, ec);
    if (ec)
      _GLIBCXX_THROW_OR_ABORT(filesystem_error(
	      "directory iterator cannot advance", ec));
    return ok;
  }

  bool should_recurse(bool follow_symlink, error_code& ec) const
  {
    file_type type = entry._M_type;
    if (type == file_type::none)
    {
      type = entry.symlink_status(ec).type();
      if (ec)
	return false;
    }

    if (type == file_type::directory)
      return true;
    if (type == file_type::symlink)
      return follow_symlink && is_directory(entry.status(ec));
    return false;
  }

  // Return a pathname for the current directory entry, as an _At_path.
  _Dir_base::_At_path
  current() const noexcept
  {
    const fs::path& p = entry.path();
#if _GLIBCXX_HAVE_DIRFD
    if (!p.empty()) [[__likely__]]
      {
	auto len = std::prev(p.end())->native().size();
	return {::dirfd(this->dirp), p.c_str(), p.native().size() - len};
      }
#endif
    return p.c_str();
  }

  // Create a new _Dir for the directory this->entry.path().
  _Dir
  open_subdir(bool skip_permission_denied, bool nofollow,
	      error_code& ec) const noexcept
  {
    _Dir_base d(current(), skip_permission_denied, nofollow, ec);
    // If this->path is empty, the new _Dir should have an empty path too.
    const fs::path& p = this->path.empty() ? this->path : this->entry.path();
    return _Dir(std::move(d), p);
  }

  bool
  do_unlink(bool is_directory, error_code& ec) const noexcept
  {
#if _GLIBCXX_HAVE_UNLINKAT
    const auto atp = current();
    if (::unlinkat(atp.dir(), atp.path_at_dir(),
		   is_directory ? AT_REMOVEDIR : 0) == -1)
      {
	ec.assign(errno, std::generic_category());
	return false;
      }
    else
      {
	ec.clear();
	return true;
      }
#else
    return fs::remove(entry.path(), ec);
#endif
  }

  // Remove the non-directory that this->entry refers to.
  bool
  unlink(error_code& ec) const noexcept
  { return do_unlink(/* is_directory*/ false, ec); }

  // Remove the directory that this->entry refers to.
  bool
  rmdir(error_code& ec) const noexcept
  { return do_unlink(/* is_directory*/ true, ec); }

  fs::path		path; // Empty if only using unlinkat with file descr.
  directory_entry	entry;
};

namespace
{
  template<typename Bitmask>
    inline bool
    is_set(Bitmask obj, Bitmask bits)
    {
      return (obj & bits) != Bitmask::none;
    }

// Non-standard directory option flags, currently only for internal use:
//
// Do not allow directory iterator to open a symlink.
// This might seem redundant given directory_options::follow_directory_symlink
// but that is only checked for recursing into sub-directories, and we need
// something that controls the initial opendir() call in the constructor.
constexpr fs::directory_options __directory_iterator_nofollow{64};
// Do not store full paths in std::filesystem::recursive_directory_iterator.
// When fs::remove_all uses recursive_directory_iterator::__erase and unlinkat
// is available in libc, we do not need the parent directory's path, only the
// filenames of the directory entries (and a file descriptor for the parent).
// This flag avoids allocating memory for full paths that won't be needed.
constexpr fs::directory_options __directory_iterator_filename_only{128};
}

fs::directory_iterator::
directory_iterator(const path& p, directory_options options, error_code* ecptr)
{
  // Do not report an error for permission denied errors.
  const bool skip_permission_denied
    = is_set(options, directory_options::skip_permission_denied);
  // Do not allow opening a symlink.
  const bool nofollow = is_set(options, __directory_iterator_nofollow);

  error_code ec;
  _Dir dir(p, skip_permission_denied, nofollow, /*filename only*/false, ec);

  if (dir.dirp)
    {
      auto sp = std::__make_shared<fs::_Dir>(std::move(dir));
      if (sp->advance(skip_permission_denied, ec))
	_M_dir.swap(sp);
    }
  if (ecptr)
    *ecptr = ec;
  else if (ec)
    _GLIBCXX_THROW_OR_ABORT(fs::filesystem_error(
	  "directory iterator cannot open directory", p, ec));
}

const fs::directory_entry&
fs::directory_iterator::operator*() const noexcept
{
  return _M_dir->entry;
}

fs::directory_iterator&
fs::directory_iterator::operator++()
{
  if (!_M_dir)
    _GLIBCXX_THROW_OR_ABORT(filesystem_error(
	  "cannot advance non-dereferenceable directory iterator",
	  std::make_error_code(errc::invalid_argument)));
  if (!_M_dir->advance())
    _M_dir.reset();
  return *this;
}

fs::directory_iterator&
fs::directory_iterator::increment(error_code& ec)
{
  if (!_M_dir)
    {
      ec = std::make_error_code(errc::invalid_argument);
      return *this;
    }
  if (!_M_dir->advance(ec))
    _M_dir.reset();
  return *this;
}

struct fs::recursive_directory_iterator::_Dir_stack : std::stack<_Dir>
{
  _Dir_stack(directory_options opts, _Dir&& dir)
  : options(opts), pending(true)
  {
    this->push(std::move(dir));
  }

  path::string_type orig;
  const directory_options options;
  bool pending;

  void clear() { c.clear(); }

  path current_path() const
  {
    path p;
    if (top().path.empty())
      {
	// Reconstruct path that failed from dir stack.
	p = orig;
	for (auto& d : this->c)
	  p /= d.entry.path();
      }
    else
      p = top().entry.path();
    return p;
  }
};

fs::recursive_directory_iterator::
recursive_directory_iterator(const path& p, directory_options options,
                             error_code* ecptr)
{
  // Do not report an error for permission denied errors.
  const bool skip_permission_denied
    = is_set(options, directory_options::skip_permission_denied);
  // Do not allow opening a symlink as the starting directory.
  const bool nofollow = is_set(options, __directory_iterator_nofollow);
  // Prefer to store only filenames (not full paths) in directory_entry values.
  const bool filename_only
     = is_set(options, __directory_iterator_filename_only);

  error_code ec;
  _Dir dir(p, skip_permission_denied, nofollow, filename_only, ec);

  if (dir.dirp)
    {
      auto sp = std::__make_shared<_Dir_stack>(options, std::move(dir));
      if (ecptr ? sp->top().advance(skip_permission_denied, *ecptr)
		: sp->top().advance(skip_permission_denied))
	{
	  _M_dirs.swap(sp);
	  if (filename_only) // Need to save original path for error reporting.
	    _M_dirs->orig = p.native();
	}
    }
  else if (ecptr)
    *ecptr = ec;
  else if (ec)
    _GLIBCXX_THROW_OR_ABORT(fs::filesystem_error(
	  "recursive directory iterator cannot open directory", p, ec));
}

fs::recursive_directory_iterator::~recursive_directory_iterator() = default;

fs::directory_options
fs::recursive_directory_iterator::options() const noexcept
{
  return _M_dirs->options;
}

int
fs::recursive_directory_iterator::depth() const noexcept
{
  return int(_M_dirs->size()) - 1;
}

bool
fs::recursive_directory_iterator::recursion_pending() const noexcept
{
  return _M_dirs->pending;
}

const fs::directory_entry&
fs::recursive_directory_iterator::operator*() const noexcept
{
  return _M_dirs->top().entry;
}

fs::recursive_directory_iterator&
fs::recursive_directory_iterator::
operator=(const recursive_directory_iterator& other) noexcept = default;

fs::recursive_directory_iterator&
fs::recursive_directory_iterator::
operator=(recursive_directory_iterator&& other) noexcept = default;

fs::recursive_directory_iterator&
fs::recursive_directory_iterator::operator++()
{
  error_code ec;
  increment(ec);
  if (ec)
    _GLIBCXX_THROW_OR_ABORT(filesystem_error(
	  "cannot increment recursive directory iterator", ec));
  return *this;
}

fs::recursive_directory_iterator&
fs::recursive_directory_iterator::increment(error_code& ec)
{
  if (!_M_dirs)
    {
      ec = std::make_error_code(errc::invalid_argument);
      return *this;
    }

  const bool follow
    = is_set(_M_dirs->options, directory_options::follow_directory_symlink);
  const bool skip_permission_denied
    = is_set(_M_dirs->options, directory_options::skip_permission_denied);

  auto& top = _M_dirs->top();

  if (std::exchange(_M_dirs->pending, true) && top.should_recurse(follow, ec))
    {
      _Dir dir = top.open_subdir(skip_permission_denied, !follow, ec);
      if (ec)
	{
	  _M_dirs.reset();
	  return *this;
	}
      if (dir.dirp)
	_M_dirs->push(std::move(dir));
    }

  while (!_M_dirs->top().advance(skip_permission_denied, ec) && !ec)
    {
      _M_dirs->pop();
      if (_M_dirs->empty())
	{
	  _M_dirs.reset();
	  return *this;
	}
    }

  if (ec)
    _M_dirs.reset();

  return *this;
}

void
fs::recursive_directory_iterator::pop(error_code& ec)
{
  if (!_M_dirs)
    {
      ec = std::make_error_code(errc::invalid_argument);
      return;
    }

  const bool skip_permission_denied
    = is_set(_M_dirs->options, directory_options::skip_permission_denied);

  do {
    _M_dirs->pop();
    if (_M_dirs->empty())
      {
	_M_dirs.reset();
	ec.clear();
	return;
      }
  } while (!_M_dirs->top().advance(skip_permission_denied, ec) && !ec);

  if (ec)
    _M_dirs.reset();
}

void
fs::recursive_directory_iterator::pop()
{
  [[maybe_unused]] const bool dereferenceable = _M_dirs != nullptr;
  error_code ec;
  pop(ec);
  if (ec)
    _GLIBCXX_THROW_OR_ABORT(filesystem_error(dereferenceable
	  ? "recursive directory iterator cannot pop"
	  : "non-dereferenceable recursive directory iterator cannot pop",
	  ec));
}

void
fs::recursive_directory_iterator::disable_recursion_pending() noexcept
{
  _M_dirs->pending = false;
}

// Used to implement filesystem::remove_all.
fs::recursive_directory_iterator&
fs::recursive_directory_iterator::__erase(error_code* ecptr)
{
  error_code ec;
  if (!_M_dirs)
    {
      ec = std::make_error_code(errc::invalid_argument);
      return *this;
    }

  // We never want to skip permission denied when removing files.
  const bool skip_permission_denied = false;
  // We never want to follow directory symlinks when removing files.
  const bool nofollow = true;

  // Loop until we find something we can remove.
  while (!ec)
    {
      auto& top = _M_dirs->top();

#if _GLIBCXX_FILESYSTEM_IS_WINDOWS
      // _Dir::unlink uses fs::remove which uses std::system_category() for
      // Windows error codes, so we can't just check for EPERM and EISDIR.
      // Use directory_entry::refresh() here to check if we have a directory.
      // This can be a TOCTTOU race, but we don't have openat or unlinkat to
      // solve that on Windows, and generally don't support symlinks anyway.
      if (top.entry._M_type == file_type::none)
	top.entry.refresh();
#endif

      if (top.entry._M_type == file_type::directory)
	{
	  _Dir dir = top.open_subdir(skip_permission_denied, nofollow, ec);
	  if (!ec)
	    {
	      __glibcxx_assert(dir.dirp != nullptr);
	      if (dir.advance(skip_permission_denied, ec))
		{
		  // Non-empty directory, recurse into it.
		  _M_dirs->push(std::move(dir));
		  continue;
		}
	      if (!ec)
		{
		  // Directory is empty so we can remove it.
		  if (top.rmdir(ec))
		    break; // Success
		}
	    }
	}
      else if (top.unlink(ec))
	break; // Success
#if ! _GLIBCXX_FILESYSTEM_IS_WINDOWS
      else if (top.entry._M_type == file_type::none)
	{
	  // We did not have a cached type, so it's possible that top.entry
	  // is actually a directory, and that's why the unlink above failed.
#ifdef EPERM
	  // POSIX.1-2017 says unlink on a directory returns EPERM,
	  // but LSB allows EISDIR too. Some targets don't even define EPERM.
	  if (ec.value() == EPERM || ec.value() == EISDIR)
#else
	  if (ec.value() == EISDIR)
#endif
	    {
	      // Retry, treating it as a directory.
	      top.entry._M_type = file_type::directory;
	      ec.clear();
	      continue;
	    }
	}
#endif
    }

  if (!ec)
    {
      // We successfully removed the current entry, so advance to the next one.
      if (_M_dirs->top().advance(skip_permission_denied, ec))
	return *this;
      else if (!ec)
	{
	  // Reached the end of the current directory.
	  _M_dirs->pop();
	  if (_M_dirs->empty())
	    _M_dirs.reset();
	  return *this;
	}
    }

  // Reset _M_dirs to empty.
  auto dirs = std::move(_M_dirs);

  // Need to report an error
  if (ecptr)
    *ecptr = ec;
  else
    _GLIBCXX_THROW_OR_ABORT(fs::filesystem_error("cannot remove all",
						 dirs->orig,
						 dirs->current_path(),
						 ec));

  return *this;
}
