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

#include "chrome/utility/image_writer/image_writer.h"

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOBSD.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/storage/IOBlockStorageDevice.h>
#include <IOKit/storage/IOMedia.h>
#include <IOKit/storage/IOStorageProtocolCharacteristics.h>
#include <stddef.h>
#include <sys/socket.h>

#include <memory>

#include "base/apple/scoped_cftyperef.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/mac/scoped_ioobject.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/extensions/image_writer/image_writer_util_mac.h"
#include "chrome/utility/image_writer/disk_unmounter_mac.h"
#include "chrome/utility/image_writer/error_message_strings.h"

namespace image_writer {

static const char kAuthOpenPath[] = "/usr/libexec/authopen";

bool ImageWriter::IsValidDevice() {
  base::apple::ScopedCFTypeRef<CFStringRef> cf_bsd_name(
      base::SysUTF8ToCFStringRef(device_path_.value()));
  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> matching(
      IOServiceMatching(kIOMediaClass));
  CFDictionaryAddValue(matching.get(), CFSTR(kIOMediaWholeKey), kCFBooleanTrue);
  CFDictionaryAddValue(matching.get(), CFSTR(kIOMediaWritableKey),
                       kCFBooleanTrue);
  CFDictionaryAddValue(matching.get(), CFSTR(kIOBSDNameKey), cf_bsd_name.get());

  // IOServiceGetMatchingService consumes a reference to the matching dictionary
  // passed to it.
  base::mac::ScopedIOObject<io_service_t> disk_obj(
      IOServiceGetMatchingService(kIOMainPortDefault, matching.release()));
  if (!disk_obj)
    return false;

  return extensions::IsSuitableRemovableStorageDevice(disk_obj.get(), nullptr,
                                                      nullptr, nullptr);
}

void ImageWriter::UnmountVolumes(base::OnceClosure continuation) {
  if (!unmounter_)
    unmounter_ = std::make_unique<DiskUnmounterMac>();

  unmounter_->Unmount(
      device_path_.value(), std::move(continuation),
      base::BindOnce(&ImageWriter::Error, base::Unretained(this),
                     error::kUnmountVolumes));
}

bool ImageWriter::OpenDevice() {
  base::LaunchOptions options;
  options.wait = false;

  // Create a socket pair for communication.
  int sockets[2];
  int result = socketpair(AF_UNIX, SOCK_STREAM, 0, sockets);
  if (result == -1) {
    PLOG(ERROR) << "Unable to allocate socket pair.";
    return false;
  }
  base::ScopedFD parent_socket(sockets[0]);
  base::ScopedFD child_socket(sockets[1]);

  // Map the client socket to the client's STDOUT.
  options.fds_to_remap.push_back(
      std::pair<int, int>(child_socket.get(), STDOUT_FILENO));

  // Find the file path to open.
  base::FilePath real_device_path;
  if (device_path_.IsAbsolute()) {
    // This only occurs for tests where the device path is mocked with a
    // temporary file.
    real_device_path = device_path_;
  } else {
    // Get the raw device file. Writes need to be in multiples of
    // DAMediaBlockSize (usually 512). This is fine since WriteChunk() writes in
    // multiples of kMemoryAlignment.
    real_device_path =
        base::FilePath("/dev").Append("r" + device_path_.BaseName().value());
  }

  // Build the command line.
  std::string rdwr = base::NumberToString(O_RDWR);

  base::CommandLine cmd_line = base::CommandLine(base::FilePath(kAuthOpenPath));
  cmd_line.AppendSwitch("-stdoutpipe");
  // Using AppendSwitchNative will use an equal-symbol which we don't want.
  cmd_line.AppendArg("-o");
  cmd_line.AppendArg(rdwr);
  cmd_line.AppendArgPath(real_device_path);

  // Launch the process.
  base::Process process = base::LaunchProcess(cmd_line, options);
  if (!process.IsValid()) {
    LOG(ERROR) << "Failed to launch authopen process.";
    return false;
  }

  // Receive a file descriptor from authopen which sends a single FD via
  // sendmsg and the SCM_RIGHTS extension.
  int fd = -1;
  const size_t kDataBufferSize = sizeof(struct cmsghdr) + sizeof(int);
  char data_buffer[kDataBufferSize];

  struct iovec io_vec[1];
  io_vec[0].iov_base = data_buffer;
  io_vec[0].iov_len = kDataBufferSize;

  const socklen_t kCmsgSocketSize =
      static_cast<socklen_t>(CMSG_SPACE(sizeof(int)));
  char cmsg_socket[kCmsgSocketSize];

  struct msghdr message = {0};
  message.msg_iov = io_vec;
  message.msg_iovlen = 1;
  message.msg_control = cmsg_socket;
  message.msg_controllen = kCmsgSocketSize;

  ssize_t size = HANDLE_EINTR(recvmsg(parent_socket.get(), &message, 0));
  if (size > 0) {
    struct cmsghdr* cmsg_socket_header = CMSG_FIRSTHDR(&message);

    if (cmsg_socket_header && cmsg_socket_header->cmsg_level == SOL_SOCKET &&
        cmsg_socket_header->cmsg_type == SCM_RIGHTS) {
      fd = *reinterpret_cast<int*>(UNSAFE_TODO(CMSG_DATA(cmsg_socket_header)));
    }
  }

  device_file_ = base::File(fd);

  // Wait for the child.
  int child_exit_status;
  if (!process.WaitForExit(&child_exit_status)) {
    LOG(ERROR) << "Unable to wait for child.";
    return false;
  }

  if (child_exit_status) {
    LOG(ERROR) << "Child process returned failure.";
    return false;
  }

  return device_file_.IsValid();
}

}  // namespace image_writer
