// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "brillo/files/safe_fd.h"

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/posix/eintr_wrapper.h>
#include <brillo/files/file_util.h>
#include <brillo/files/scoped_dir.h>
#include <brillo/syslog_logging.h>

namespace brillo {

namespace {

SafeFD::SafeFDResult MakeErrorResult(SafeFD::Error error) {
  return std::make_pair(SafeFD(), error);
}

SafeFD::SafeFDResult MakeSuccessResult(SafeFD&& fd) {
  return std::make_pair(std::move(fd), SafeFD::Error::kNoError);
}

SafeFD::SafeFDResult OpenPathComponentInternal(int parent_fd,
                                               const std::string& file,
                                               int flags,
                                               mode_t mode) {
  if (file != "/" && file.find("/") != std::string::npos) {
    return MakeErrorResult(SafeFD::Error::kBadArgument);
  }
  SafeFD fd;

  // O_NONBLOCK is used to avoid hanging on edge cases (e.g. a serial port with
  // flow control, or a FIFO without a writer).
  if (parent_fd >= 0 || parent_fd == AT_FDCWD) {
    fd.UnsafeReset(HANDLE_EINTR(openat(parent_fd, file.c_str(),
                                       flags | O_NONBLOCK | O_NOFOLLOW, mode)));
  } else if (file == "/") {
    fd.UnsafeReset(HANDLE_EINTR(open(
        file.c_str(), flags | O_DIRECTORY | O_NONBLOCK | O_NOFOLLOW, mode)));
  }

  if (!fd.is_valid()) {
    // open(2) fails with ELOOP when the last component of the |path| is a
    // symlink. It fails with ENXIO when |path| is a FIFO and |flags| is for
    // writing because of the O_NONBLOCK flag added above.
    switch (errno) {
      case ENOENT:
        // Do not write to the log because opening a non-existent file is a
        // frequent occurrence.
        return MakeErrorResult(SafeFD::Error::kDoesNotExist);
      case ELOOP:
        // PLOG prints something along the lines of the symlink depth being too
        // great which is is misleading so LOG is used instead.
        LOG(ERROR) << "Symlink detected! failed to open \"" << file
                   << "\" safely.";
        return MakeErrorResult(SafeFD::Error::kSymlinkDetected);
      case EISDIR:
        PLOG(ERROR) << "Directory detected! failed to open \"" << file
                    << "\" safely";
        return MakeErrorResult(SafeFD::Error::kWrongType);
      case ENOTDIR:
        PLOG(ERROR) << "Not a directory! failed to open \"" << file
                    << "\" safely";
        return MakeErrorResult(SafeFD::Error::kWrongType);
      case ENXIO:
        PLOG(ERROR) << "FIFO detected! failed to open \"" << file
                    << "\" safely";
        return MakeErrorResult(SafeFD::Error::kWrongType);
      default:
        PLOG(ERROR) << "Failed to open \"" << file << '"';
        return MakeErrorResult(SafeFD::Error::kIOError);
    }
  }

  // Remove the O_NONBLOCK flag unless the original |flags| have it.
  if ((flags & O_NONBLOCK) == 0) {
    flags = fcntl(fd.get(), F_GETFL);
    if (flags == -1) {
      PLOG(ERROR) << "Failed to get fd flags for " << file;
      return MakeErrorResult(SafeFD::Error::kIOError);
    }
    if (fcntl(fd.get(), F_SETFL, flags & ~O_NONBLOCK)) {
      PLOG(ERROR) << "Failed to set fd flags for " << file;
      return MakeErrorResult(SafeFD::Error::kIOError);
    }
  }

  return MakeSuccessResult(std::move(fd));
}

SafeFD::SafeFDResult OpenSafelyInternal(int parent_fd,
                                        const base::FilePath& path,
                                        int flags,
                                        mode_t mode) {
  std::vector<std::string> components;
  path.GetComponents(&components);

  auto itr = components.begin();
  if (itr == components.end()) {
    LOG(ERROR) << "A path is required.";
    return MakeErrorResult(SafeFD::Error::kBadArgument);
  }

  SafeFD::SafeFDResult child_fd;
  int parent_flags = flags | O_NONBLOCK | O_RDONLY | O_DIRECTORY | O_PATH;
  for (; itr + 1 != components.end(); ++itr) {
    child_fd = OpenPathComponentInternal(parent_fd, *itr, parent_flags, 0);
    // Operation failed, so directly return the error result.
    if (!child_fd.first.is_valid()) {
      return child_fd;
    }
    parent_fd = child_fd.first.get();
  }

  return OpenPathComponentInternal(parent_fd, *itr, flags, mode);
}

SafeFD::Error CheckAttributes(int fd,
                              mode_t permissions,
                              uid_t uid,
                              gid_t gid) {
  struct stat fd_attributes;
  if (fstat(fd, &fd_attributes) != 0) {
    PLOG(ERROR) << "fstat failed";
    return SafeFD::Error::kIOError;
  }

  if (fd_attributes.st_uid != uid) {
    LOG(ERROR) << "Owner uid is " << fd_attributes.st_uid << " instead of "
               << uid;
    return SafeFD::Error::kWrongUID;
  }

  if (fd_attributes.st_gid != gid) {
    LOG(ERROR) << "Owner gid is " << fd_attributes.st_gid << " instead of "
               << gid;
    return SafeFD::Error::kWrongGID;
  }

  if ((0777 & (fd_attributes.st_mode ^ permissions)) != 0) {
    mode_t mask = umask(0);
    umask(mask);
    LOG(ERROR) << "Permissions are " << std::oct
               << (0777 & fd_attributes.st_mode) << " instead of "
               << (0777 & permissions) << ". Umask is " << std::oct << mask
               << std::dec;
    return SafeFD::Error::kWrongPermissions;
  }

  return SafeFD::Error::kNoError;
}

SafeFD::Error GetFileSize(int fd, size_t* file_size) {
  struct stat fd_attributes;
  if (fstat(fd, &fd_attributes) != 0) {
    return SafeFD::Error::kIOError;
  }

  *file_size = fd_attributes.st_size;
  return SafeFD::Error::kNoError;
}

}  // namespace

bool SafeFD::IsError(SafeFD::Error err) {
  return err != Error::kNoError;
}

const char* SafeFD::RootPath = "/";

SafeFD::SafeFDResult SafeFD::Root() {
  SafeFD::SafeFDResult root =
      OpenPathComponentInternal(-1, "/", O_DIRECTORY, 0);
  if (strcmp(SafeFD::RootPath, "/") == 0) {
    return root;
  }

  if (!root.first.is_valid()) {
    LOG(ERROR) << "Failed to open root directory!";
    return root;
  }
  return root.first.OpenExistingDir(base::FilePath(SafeFD::RootPath));
}

void SafeFD::SetRootPathForTesting(const char* new_root_path) {
  SafeFD::RootPath = new_root_path;
}

int SafeFD::get() const {
  return fd_.get();
}

bool SafeFD::is_valid() const {
  return fd_.is_valid();
}

void SafeFD::reset() {
  return fd_.reset();
}

void SafeFD::UnsafeReset(int fd) {
  return fd_.reset(fd);
}

SafeFD::Error SafeFD::Write(const char* data, size_t size) {
  if (!fd_.is_valid()) {
    return SafeFD::Error::kNotInitialized;
  }
  errno = 0;
  if (!base::WriteFileDescriptor(fd_.get(), data, size)) {
    PLOG(ERROR) << "Failed to write to file";
    return SafeFD::Error::kIOError;
  }

  if (HANDLE_EINTR(ftruncate(fd_.get(), size)) != 0) {
    PLOG(ERROR) << "Failed to truncate file";
    return SafeFD::Error::kIOError;
  }
  return SafeFD::Error::kNoError;
}

std::pair<std::vector<char>, SafeFD::Error> SafeFD::ReadContents(
    size_t max_size) {
  std::vector<char> buffer;
  if (!fd_.is_valid()) {
    return std::make_pair(std::move(buffer), SafeFD::Error::kNotInitialized);
  }

  size_t file_size = 0;
  SafeFD::Error err = GetFileSize(fd_.get(), &file_size);
  if (IsError(err)) {
    return std::make_pair(std::move(buffer), err);
  }

  if (file_size > max_size) {
    return std::make_pair(std::move(buffer), SafeFD::Error::kExceededMaximum);
  }

  buffer.resize(file_size);

  err = Read(buffer.data(), buffer.size());
  if (IsError(err)) {
    buffer.clear();
  }
  return std::make_pair(std::move(buffer), err);
}

SafeFD::Error SafeFD::Read(char* data, size_t size) {
  if (!fd_.is_valid()) {
    return SafeFD::Error::kNotInitialized;
  }

  if (!base::ReadFromFD(fd_.get(), data, size)) {
    PLOG(ERROR) << "Failed to read file";
    return SafeFD::Error::kIOError;
  }
  return SafeFD::Error::kNoError;
}

SafeFD::SafeFDResult SafeFD::OpenExistingFile(const base::FilePath& path,
                                              int flags) {
  if (!fd_.is_valid()) {
    return MakeErrorResult(SafeFD::Error::kNotInitialized);
  }

  return OpenSafelyInternal(get(), path, flags, 0 /*mode*/);
}

SafeFD::SafeFDResult SafeFD::OpenExistingDir(const base::FilePath& path,
                                             int flags) {
  if (!fd_.is_valid()) {
    return MakeErrorResult(SafeFD::Error::kNotInitialized);
  }

  return OpenSafelyInternal(get(), path, O_DIRECTORY | flags /*flags*/,
                            0 /*mode*/);
}

SafeFD::SafeFDResult SafeFD::MakeFile(const base::FilePath& path,
                                      mode_t permissions,
                                      uid_t uid,
                                      gid_t gid,
                                      int flags) {
  if (!fd_.is_valid()) {
    return MakeErrorResult(SafeFD::Error::kNotInitialized);
  }

  // Open (and create if necessary) the parent directory.
  base::FilePath dir_name = path.DirName();
  SafeFD::SafeFDResult parent_dir;
  int parent_dir_fd = get();
  if (!dir_name.empty() &&
      dir_name.value() != base::FilePath::kCurrentDirectory) {
    // Apply execute permission where read permission are present for parent
    // directories.
    int dir_permissions = permissions | ((permissions & 0444) >> 2);
    parent_dir =
        MakeDir(dir_name, dir_permissions, uid, gid, O_RDONLY | O_CLOEXEC);
    if (!parent_dir.first.is_valid()) {
      return parent_dir;
    }
    parent_dir_fd = parent_dir.first.get();
  }

  // If file already exists, validate permissions.
  SafeFDResult file = OpenPathComponentInternal(
      parent_dir_fd, path.BaseName().value(), flags, permissions /*mode*/);
  if (file.first.is_valid()) {
    SafeFD::Error err =
        CheckAttributes(file.first.get(), permissions, uid, gid);
    if (IsError(err)) {
      return MakeErrorResult(err);
    }
    return file;
  } else if (errno != ENOENT) {
    return file;
  }

  // The file does exist, create it and set the ownership.
  file =
      OpenPathComponentInternal(parent_dir_fd, path.BaseName().value(),
                                O_CREAT | O_EXCL | flags, permissions /*mode*/);
  if (!file.first.is_valid()) {
    return file;
  }
  if (HANDLE_EINTR(fchown(file.first.get(), uid, gid)) != 0) {
    PLOG(ERROR) << "Failed to set ownership in MakeFile() for \""
                << path.value() << '"';
    return MakeErrorResult(SafeFD::Error::kIOError);
  }
  return file;
}

SafeFD::SafeFDResult SafeFD::MakeDir(const base::FilePath& path,
                                     mode_t permissions,
                                     uid_t uid,
                                     gid_t gid,
                                     int flags) {
  if (!fd_.is_valid()) {
    return MakeErrorResult(SafeFD::Error::kNotInitialized);
  }

  std::vector<std::string> components;
  path.GetComponents(&components);
  if (components.empty()) {
    LOG(ERROR) << "Called MakeDir() with an empty path";
    return MakeErrorResult(SafeFD::Error::kBadArgument);
  }

  // Walk the path creating directories as necessary.
  SafeFD dir;
  SafeFDResult child_dir;
  int parent_dir_fd = get();
  int dir_flags = O_NONBLOCK | O_DIRECTORY | O_PATH;
  bool made_dir = false;
  for (const auto& component : components) {
    if (mkdirat(parent_dir_fd, component.c_str(), permissions) != 0) {
      if (errno != EEXIST) {
        PLOG(ERROR) << "Failed to mkdirat() " << component << ": full_path=\""
                    << path.value() << '"';
        return MakeErrorResult(SafeFD::Error::kIOError);
      }
    } else {
      made_dir = true;
    }

    // For the last component in the path, use the flags provided by the caller.
    if (&component == &components.back()) {
      dir_flags = flags | O_DIRECTORY;
    }
    child_dir = OpenPathComponentInternal(parent_dir_fd, component, dir_flags,
                                          0 /*mode*/);
    if (!child_dir.first.is_valid()) {
      return child_dir;
    }

    dir = std::move(child_dir.first);
    parent_dir_fd = dir.get();
  }

  if (made_dir) {
    // If the directory was created, set the ownership.
    if (HANDLE_EINTR(fchown(dir.get(), uid, gid)) != 0) {
      PLOG(ERROR) << "Failed to set ownership in MakeDir() for \""
                  << path.value() << '"';
      return MakeErrorResult(SafeFD::Error::kIOError);
    }
  }
  // If the directory already existed, validate the permissions.
  SafeFD::Error err = CheckAttributes(dir.get(), permissions, uid, gid);
  if (IsError(err)) {
    return MakeErrorResult(err);
  }

  return MakeSuccessResult(std::move(dir));
}

SafeFD::Error SafeFD::Link(const SafeFD& source_dir,
                           const std::string& source_name,
                           const std::string& destination_name) {
  if (!fd_.is_valid() || !source_dir.is_valid()) {
    return SafeFD::Error::kNotInitialized;
  }

  SafeFD::Error err = IsValidFilename(source_name);
  if (IsError(err)) {
    return err;
  }

  err = IsValidFilename(destination_name);
  if (IsError(err)) {
    return err;
  }

  if (HANDLE_EINTR(linkat(source_dir.get(), source_name.c_str(), fd_.get(),
                          destination_name.c_str(), 0)) != 0) {
    PLOG(ERROR) << "Failed to link \"" << destination_name << "\"";
    return SafeFD::Error::kIOError;
  }
  return SafeFD::Error::kNoError;
}

SafeFD::Error SafeFD::Unlink(const std::string& name) {
  if (!fd_.is_valid()) {
    return SafeFD::Error::kNotInitialized;
  }

  SafeFD::Error err = IsValidFilename(name);
  if (IsError(err)) {
    return err;
  }

  if (HANDLE_EINTR(unlinkat(fd_.get(), name.c_str(), 0 /*flags*/)) != 0) {
    PLOG(ERROR) << "Failed to unlink \"" << name << "\"";
    return SafeFD::Error::kIOError;
  }
  return SafeFD::Error::kNoError;
}

SafeFD::Error SafeFD::Rmdir(const std::string& name,
                            bool recursive,
                            size_t max_depth,
                            bool keep_going) {
  if (!fd_.is_valid()) {
    return SafeFD::Error::kNotInitialized;
  }

  if (max_depth == 0) {
    return SafeFD::Error::kExceededMaximum;
  }

  SafeFD::Error err = IsValidFilename(name);
  if (IsError(err)) {
    return err;
  }

  SafeFD::Error last_err = SafeFD::Error::kNoError;

  if (recursive) {
    SafeFD dir_fd;
    std::tie(dir_fd, err) =
        OpenPathComponentInternal(fd_.get(), name, O_DIRECTORY, 0);
    if (!dir_fd.is_valid()) {
      return err;
    }

    // The ScopedDIR takes ownership of this so dup_fd is not scoped on its own.
    int dup_fd = dup(dir_fd.get());
    if (dup_fd < 0) {
      PLOG(ERROR) << "dup failed";
      return SafeFD::Error::kIOError;
    }

    ScopedDIR dir(fdopendir(dup_fd));
    if (!dir.is_valid()) {
      PLOG(ERROR) << "fdopendir failed";
      close(dup_fd);
      return SafeFD::Error::kIOError;
    }

    struct stat dir_info;
    if (fstat(dir_fd.get(), &dir_info) != 0) {
      return SafeFD::Error::kIOError;
    }

    errno = 0;
    const dirent* entry = HANDLE_EINTR_IF_EQ(readdir(dir.get()), nullptr);
    while (entry != nullptr) {
      SafeFD::Error err = [&]() {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
          return SafeFD::Error::kNoError;
        }

        struct stat child_info;
        if (fstatat(dir_fd.get(), entry->d_name, &child_info,
                    AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW) != 0) {
          return SafeFD::Error::kIOError;
        }

        if (child_info.st_dev != dir_info.st_dev) {
          return SafeFD::Error::kBoundaryDetected;
        }

        if (entry->d_type != DT_DIR) {
          return dir_fd.Unlink(entry->d_name);
        }

        return dir_fd.Rmdir(entry->d_name, true, max_depth - 1, keep_going);
      }();

      if (IsError(err)) {
        if (!keep_going) {
          return err;
        }
        last_err = err;
      }

      errno = 0;
      entry = HANDLE_EINTR_IF_EQ(readdir(dir.get()), nullptr);
    }
    if (errno != 0) {
      PLOG(ERROR) << "readdir failed";
      return SafeFD::Error::kIOError;
    }
  }

  if (HANDLE_EINTR(unlinkat(fd_.get(), name.c_str(), AT_REMOVEDIR)) != 0) {
    PLOG(ERROR) << "unlinkat failed";
    if (errno == ENOTDIR) {
      return SafeFD::Error::kWrongType;
    }
    // If there was an error during the recursive delete, we expect unlink
    // to fail with ENOTEMPTY and we bubble the error from recursion
    // instead.
    if (IsError(last_err) && errno == ENOTEMPTY) {
      return last_err;
    }
    return SafeFD::Error::kIOError;
  }

  return last_err;
}

}  // namespace brillo
