// Copyright 2012 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/browser/mac/install_from_dmg.h"

#import <AppKit/AppKit.h>
#include <ApplicationServices/ApplicationServices.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#include <DiskArbitration/DiskArbitration.h>
#include <IOKit/IOKitLib.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <unistd.h>

#include <algorithm>
#include <string_view>

#include "base/apple/bridging.h"
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/apple/osstatus_logging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/mac/authorization_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_authorizationref.h"
#include "base/mac/scoped_ioobject.h"
#include "base/memory/scoped_policy.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#import "chrome/browser/mac/dock.h"
#include "chrome/browser/mac/relauncher.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"

namespace {

// Given an io_service_t (expected to be of class IOMedia), walks the ancestor
// chain, returning the closest ancestor that implements the specified class,
// if any. If no such ancestor is found, returns IO_OBJECT_NULL. Following
// the "copy" rule, the caller assumes ownership of the returned value.
//
// Note that this looks for the class by conformance, not equality. The reason
// for that is that for IOHDIXHDDrive the actual classes found will be
// IOHDIXHDDriveOutKernel for disk images mounted "out-of-kernel" or
// IOHDIXHDDriveInKernel for disk images mounted "in-kernel." (See the
// documentation for "hdiutil attach -kernel" for more information on the
// distinction.)
base::mac::ScopedIOObject<io_service_t> GetDiskImageAncestorForMedia(
    const char* disk_image_class,
    base::mac::ScopedIOObject<io_service_t> media) {
  // This is highly unlikely. media as passed in is expected to be of class
  // IOMedia. Since the media service's entire ancestor chain will be checked,
  // though, check it as well.
  if (IOObjectConformsTo(media.get(), disk_image_class)) {
    return media;
  }

  io_iterator_t iterator_ref;
  kern_return_t kr = IORegistryEntryCreateIterator(
      media.get(), kIOServicePlane,
      kIORegistryIterateRecursively | kIORegistryIterateParents, &iterator_ref);
  if (kr != KERN_SUCCESS) {
    MACH_LOG(ERROR, kr) << "IORegistryEntryCreateIterator";
    return base::mac::ScopedIOObject<io_service_t>();
  }
  base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
  iterator_ref = IO_OBJECT_NULL;

  // Look at each of the ancestor services, beginning with the parent,
  // iterating all the way up to the device tree's root. If any ancestor
  // service matches the class used for disk images, the media resides on a
  // disk image, and the disk image file's path can be determined by examining
  // the image-path property.
  for (base::mac::ScopedIOObject<io_service_t> ancestor(
           IOIteratorNext(iterator.get()));
       ancestor; ancestor.reset(IOIteratorNext(iterator.get()))) {
    if (IOObjectConformsTo(ancestor.get(), disk_image_class)) {
      return ancestor;
    }
  }

  // The media does not reside on a disk image.
  return base::mac::ScopedIOObject<io_service_t>();
}

// Given an io_service_t (expected to be of class IOMedia), determines whether
// that service is on a disk image. If it is, returns true. If image_path is
// present, it will be set to the pathname of the disk image file, encoded in
// filesystem encoding.
//
// There are two ways to do this using SPI: The first way would be to use
// DIHLCopyImageForVolume() from the DiskImages private framework.  The second
// way would be to use _kCFURLVolumeIsDiskImageKey and
// _kCFURLDiskImageBackingURLKey from CFURLPriv. However, because downstream
// users want to use Chromium as a base for code in the MAS, neither are used
// here. The request for a real API is FB9139935.
bool MediaResidesOnDiskImage(base::mac::ScopedIOObject<io_service_t> media,
                             std::string* image_path) {
  if (image_path) {
    image_path->clear();
  }

  // The IOMedia has an ancestor of type "AppleDiskImageDevice" that has a
  // property "DiskImageURL" of string type.
  base::mac::ScopedIOObject<io_service_t> di_device =
      GetDiskImageAncestorForMedia("AppleDiskImageDevice", media);
  if (di_device) {
    if (image_path) {
      base::apple::ScopedCFTypeRef<CFTypeRef> disk_image_url_cftyperef(
          IORegistryEntryCreateCFProperty(di_device.get(),
                                          CFSTR("DiskImageURL"),
                                          /*allocator=*/nullptr,
                                          /*options=*/0));
      if (!disk_image_url_cftyperef) {
        LOG(ERROR) << "IORegistryEntryCreateCFProperty failed for DiskImageURL";
        return true;
      }

      CFStringRef disk_image_url_string =
          base::apple::CFCast<CFStringRef>(disk_image_url_cftyperef.get());
      if (!disk_image_url_string) {
        base::apple::ScopedCFTypeRef<CFStringRef> observed_type_cf(
            CFCopyTypeIDDescription(
                CFGetTypeID(disk_image_url_cftyperef.get())));
        LOG(ERROR) << "DiskImageURL: expected CFString, observed "
                   << base::SysCFStringRefToUTF8(observed_type_cf.get());
        return true;
      }

      base::apple::ScopedCFTypeRef<CFURLRef> disk_image_url(
          CFURLCreateWithString(
              /*allocator=*/nullptr, disk_image_url_string,
              /*baseURL=*/nullptr));
      if (!disk_image_url) {
        LOG(ERROR) << "CFURLCreateWithString failed";
        return true;
      }

      base::apple::ScopedCFTypeRef<CFStringRef> disk_image_path(
          CFURLCopyFileSystemPath(disk_image_url.get(), kCFURLPOSIXPathStyle));
      if (!disk_image_path) {
        LOG(ERROR) << "CFURLCopyFileSystemPath failed";
        return true;
      }

      *image_path = base::SysCFStringRefToUTF8(disk_image_path.get());
    }

    return true;
  }

  return false;
}

// Returns `kRunningFromDiskImage` if `path` is located on a read-only
// filesystem of a disk image, `kNotRunningFromDiskImage` if not, or `kFailure`
// in the event of an error.
RunningFromDiskImageResult IsPathOnReadOnlyDiskImage(const char path[]) {
  struct statfs statfs_buf;
  if (statfs(path, &statfs_buf) != 0) {
    PLOG(ERROR) << "statfs " << path;
    return {.status = DiskImageStatus::kFailure};
  }

  if (!(statfs_buf.f_flags & MNT_RDONLY)) {
    // Not on a read-only filesystem.
    return {.status = DiskImageStatus::kNotRunningFromDiskImage};
  }

  std::string_view dev_root("/dev/");
  std::string dmg_bsd_device_name(statfs_buf.f_mntfromname);
  if (!dmg_bsd_device_name.starts_with(dev_root)) {
    // Not rooted at dev_root, no BSD name to search on.
    return {.status = DiskImageStatus::kNotRunningFromDiskImage};
  }

  // BSD names in IOKit don't include dev_root.
  dmg_bsd_device_name = dmg_bsd_device_name.substr(dev_root.size());

  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> match_dict(
      IOBSDNameMatching(kIOMainPortDefault, /*options=*/0,
                        dmg_bsd_device_name.c_str()));
  if (!match_dict) {
    LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name;
    return {.status = DiskImageStatus::kFailure};
  }

  base::mac::ScopedIOObject<io_iterator_t> iterator;
  kern_return_t kr = IOServiceGetMatchingServices(
      kIOMainPortDefault, match_dict.release(), iterator.InitializeInto());
  if (kr != KERN_SUCCESS) {
    MACH_LOG(ERROR, kr) << "IOServiceGetMatchingServices";
    return {.status = DiskImageStatus::kFailure};
  }

  // There needs to be exactly one matching service.
  base::mac::ScopedIOObject<io_service_t> media(IOIteratorNext(iterator.get()));
  if (!media) {
    LOG(ERROR) << "IOIteratorNext: no service";
    return {.status = DiskImageStatus::kFailure};
  }
  base::mac::ScopedIOObject<io_service_t> unexpected_service(
      IOIteratorNext(iterator.get()));
  if (unexpected_service) {
    LOG(ERROR) << "IOIteratorNext: too many services";
    return {.status = DiskImageStatus::kFailure};
  }

  if (MediaResidesOnDiskImage(media, /*image_path=*/nullptr)) {
    return {.status = DiskImageStatus::kRunningFromDiskImage,
            .dmg_bsd_device_name = dmg_bsd_device_name};
  }
  return {.status = DiskImageStatus::kNotRunningFromDiskImage};
}

// Shows a dialog asking the user whether or not to install from the disk
// image.  Returns true if the user approves installation.
bool ShouldInstallDialog() {
  NSString* title = l10n_util::GetNSStringFWithFixup(
      IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
  NSString* prompt = l10n_util::GetNSStringFWithFixup(
      IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
  NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
  NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);

  NSAlert* alert = [[NSAlert alloc] init];

  alert.alertStyle = NSAlertStyleInformational;
  alert.messageText = title;
  alert.informativeText = prompt;

  [alert addButtonWithTitle:yes];
  NSButton* cancel_button = [alert addButtonWithTitle:no];
  cancel_button.keyEquivalent = @"\e";

  NSInteger result = [alert runModal];

  return result == NSAlertFirstButtonReturn;
}

// Potentially shows an authorization dialog to request authentication to copy.
// If application_directory appears to be unwritable, attempts to obtain
// authorization, which may result in the display of the dialog. Returns null if
// authorization is not performed because it does not appear to be necessary
// because the user has permission to write to application_directory. Returns
// null if authorization fails.
base::mac::ScopedAuthorizationRef MaybeShowAuthorizationDialog(
    NSString* application_directory) {
  if ([NSFileManager.defaultManager
          isWritableFileAtPath:application_directory]) {
    return base::mac::ScopedAuthorizationRef();
  }

  NSString* prompt = l10n_util::GetNSStringFWithFixup(
      IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
      l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
  return base::mac::AuthorizationCreateToRunAsRoot(
      base::apple::NSToCFPtrCast(prompt));
}

// Invokes the installer program at `installer_path` to copy `source_path` to
// `target_path` and perform any additional on-disk bookkeeping needed to be
// able to launch `target_path` properly. If `authorization_arg` is non-null,
// this function will invoke the installer with that authorization reference,
// and will attempt Keystone ticket promotion.
bool InstallFromDiskImage(base::mac::ScopedAuthorizationRef authorization,
                          NSURL* installer_url,
                          NSString* source_path,
                          NSString* target_path) {
  int exit_status;
  if (authorization) {
    const char* installer_path_c = installer_url.fileSystemRepresentation;
    const char* source_path_c = source_path.fileSystemRepresentation;
    const char* target_path_c = target_path.fileSystemRepresentation;
    const char* arguments[] = {source_path_c, target_path_c, nullptr};

    OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
        authorization, installer_path_c, kAuthorizationFlagDefaults, arguments,
        /*pipe=*/nullptr, &exit_status);
    if (status != errAuthorizationSuccess) {
      OSSTATUS_LOG(ERROR, status)
          << "AuthorizationExecuteWithPrivileges install";
      return false;
    }
  } else {
    NSError* error = nil;
    NSTask* task =
        [NSTask launchedTaskWithExecutableURL:installer_url
                                    arguments:@[ source_path, target_path ]
                                        error:&error
                           terminationHandler:nil];

    if (!task) {
      LOG(ERROR) << "NSTask launch error: "
                 << base::SysNSStringToUTF8(error.description);
      return false;
    }

    [task waitUntilExit];
    exit_status = task.terminationStatus;
  }

  if (exit_status != 0) {
    LOG(ERROR) << "install.sh: exit status " << exit_status;
    return false;
  }

  return true;
}

// Launches the application at `installed_path`. The helper application
// contained within `installed_path` will be used for the relauncher process.
// This keeps Launch Services from ever having to see or think about the helper
// application on the disk image. The relauncher process will be asked to call
// EjectAndTrashDiskImage on `dmg_bsd_device_name`.
bool LaunchInstalledApp(NSString* installed_path,
                        const std::string& dmg_bsd_device_name) {
  base::FilePath browser = base::apple::NSStringToFilePath(installed_path);

  base::FilePath helper = browser.Append("Contents/Frameworks");
  helper = helper.Append(chrome::kFrameworkName);
  helper = helper.Append("Versions");
  helper = helper.Append(chrome::kChromeVersion);
  helper = helper.Append("Helpers");
  helper = helper.Append(chrome::kHelperProcessExecutablePath);

  std::vector<std::string> args =
      base::CommandLine::ForCurrentProcess()->argv();

  std::vector<std::string> relauncher_args;
  if (!dmg_bsd_device_name.empty()) {
    std::string dmg_arg =
        base::StrCat({"--", switches::kRelauncherProcessDMGDevice, "=",
                      dmg_bsd_device_name});
    relauncher_args.push_back(dmg_arg);
  }

  return mac_relauncher::RelaunchAppAtPathWithHelper(helper, browser,
                                                     relauncher_args, args);
}

void ShowErrorDialog() {
  NSString* title =
      l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_ERROR_TITLE);
  NSString* error = l10n_util::GetNSStringFWithFixup(
      IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
  NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);

  NSAlert* alert = [[NSAlert alloc] init];

  alert.alertStyle = NSAlertStyleWarning;
  alert.messageText = title;
  alert.informativeText = error;

  [alert addButtonWithTitle:ok];

  [alert runModal];
}

}  // namespace

RunningFromDiskImageResult IsAppRunningFromReadOnlyDiskImage() {
  return IsPathOnReadOnlyDiskImage(
      base::apple::OuterBundle().bundlePath.fileSystemRepresentation);
}

bool MaybeInstallFromDiskImage() {
  @autoreleasepool {
    RunningFromDiskImageResult result = IsAppRunningFromReadOnlyDiskImage();
    if (result.status != DiskImageStatus::kRunningFromDiskImage) {
      return false;
    }

    NSArray* application_directories = NSSearchPathForDirectoriesInDomains(
        NSApplicationDirectory, NSLocalDomainMask, YES);
    if (!application_directories.count) {
      LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
                 << "no local application directories";
      return false;
    }
    NSString* application_directory = application_directories.firstObject;

    NSFileManager* file_manager = NSFileManager.defaultManager;

    BOOL is_directory;
    if (![file_manager fileExistsAtPath:application_directory
                            isDirectory:&is_directory] ||
        !is_directory) {
      VLOG(1) << "No application directory at "
              << base::SysNSStringToUTF8(application_directory);
      return false;
    }

    NSString* source_path = base::apple::OuterBundle().bundlePath;
    NSString* application_name = source_path.lastPathComponent;
    NSString* target_path =
        [application_directory stringByAppendingPathComponent:application_name];

    if ([file_manager fileExistsAtPath:target_path]) {
      VLOG(1) << "Something already exists at "
              << base::SysNSStringToUTF8(target_path);
      return false;
    }

    NSURL* installer_url =
        [base::apple::FrameworkBundle() URLForResource:@"install"
                                         withExtension:@"sh"];
    if (!installer_url) {
      VLOG(1) << "Could not locate install.sh";
      return false;
    }

    if (!ShouldInstallDialog()) {
      return false;
    }

    base::mac::ScopedAuthorizationRef authorization =
        MaybeShowAuthorizationDialog(application_directory);
    // `authorization` will be null if it's deemed unnecessary or if
    // authentication fails.  In either case, try to install without privilege
    // escalation.

    if (!InstallFromDiskImage(std::move(authorization), installer_url,
                              source_path, target_path)) {
      ShowErrorDialog();
      return false;
    }

    dock::AddIcon(target_path, source_path);

    if (result.dmg_bsd_device_name.empty()) {
      // Not fatal, just diagnostic.
      LOG(ERROR) << "Could not determine disk image BSD device name";
    }

    if (!LaunchInstalledApp(target_path, result.dmg_bsd_device_name)) {
      ShowErrorDialog();
      return false;
    }

    return true;
  }
}

namespace {

// A simple scoper that calls DASessionScheduleWithRunLoop when created and
// DASessionUnscheduleFromRunLoop when destroyed.
class ScopedDASessionScheduleWithRunLoop {
 public:
  ScopedDASessionScheduleWithRunLoop(DASessionRef session,
                                     CFRunLoopRef run_loop,
                                     CFStringRef run_loop_mode)
      : session_(session), run_loop_(run_loop), run_loop_mode_(run_loop_mode) {
    DASessionScheduleWithRunLoop(session_, run_loop_, run_loop_mode_);
  }

  ScopedDASessionScheduleWithRunLoop(
      const ScopedDASessionScheduleWithRunLoop&) = delete;
  ScopedDASessionScheduleWithRunLoop& operator=(
      const ScopedDASessionScheduleWithRunLoop&) = delete;

  ~ScopedDASessionScheduleWithRunLoop() {
    DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_);
  }

 private:
  DASessionRef session_;
  CFRunLoopRef run_loop_;
  CFStringRef run_loop_mode_;
};

// A small structure used to ferry data between SynchronousDAOperation and
// SynchronousDACallbackAdapter.
struct SynchronousDACallbackData {
  base::apple::ScopedCFTypeRef<DADissenterRef> dissenter;
  bool callback_called = false;
  bool run_loop_running = false;
  bool can_log = true;
};

// The callback target for SynchronousDAOperation. Set the fields in
// SynchronousDACallbackData properly and then stops the run loop so that
// SynchronousDAOperation may proceed.
void SynchronousDACallbackAdapter(DADiskRef disk,
                                  DADissenterRef dissenter,
                                  void* context) {
  SynchronousDACallbackData* callback_data =
      static_cast<SynchronousDACallbackData*>(context);
  callback_data->callback_called = true;

  if (dissenter) {
    callback_data->dissenter.reset(dissenter, base::scoped_policy::RETAIN);
  }

  // Only stop the run loop if SynchronousDAOperation started it. Don't stop
  // anything if this callback was reached synchronously from DADiskUnmount or
  // DADiskEject.
  if (callback_data->run_loop_running) {
    CFRunLoopStop(CFRunLoopGetCurrent());
  }
}

// Performs a DiskArbitration operation synchronously. After the operation is
// requested by SynchronousDADiskUnmount or SynchronousDADiskEject, those
// functions will call this one to run a run loop for a period of time,
// waiting for the callback to be called. When the callback is called, the
// run loop will be stopped, and this function will examine the result. If
// a dissenter prevented the operation from completing, or if the run loop
// timed out without the callback being called, this function will return
// false. When the callback completes successfully with no dissenters within
// the time allotted, this function returns true. This function requires that
// the DASession being used for the operation being performed has been added
// to the current run loop with DASessionScheduleWithRunLoop.
bool SynchronousDAOperation(const char* name,
                            SynchronousDACallbackData* callback_data) {
  // The callback may already have been called synchronously. In that case,
  // avoid spinning the run loop at all.
  if (!callback_data->callback_called) {
    const CFTimeInterval kOperationTimeoutSeconds = 15;
    base::AutoReset<bool> running_reset(&callback_data->run_loop_running, true);
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE);
  }

  if (!callback_data->callback_called) {
    LOG_IF(ERROR, callback_data->can_log) << name << ": timed out";
    return false;
  } else if (callback_data->dissenter) {
    if (callback_data->can_log) {
      CFStringRef status_string_cf =
          DADissenterGetStatusString(callback_data->dissenter.get());
      std::string status_string;
      if (status_string_cf) {
        status_string.assign(" ");
        status_string.append(base::SysCFStringRefToUTF8(status_string_cf));
      }
      LOG(ERROR) << name << ": dissenter: "
                 << DADissenterGetStatus(callback_data->dissenter.get())
                 << status_string;
    }
    return false;
  }

  return true;
}

// Calls DADiskUnmount synchronously, returning the result.
bool SynchronousDADiskUnmount(DADiskRef disk,
                              DADiskUnmountOptions options,
                              bool can_log) {
  SynchronousDACallbackData callback_data;
  callback_data.can_log = can_log;
  DADiskUnmount(disk, options, SynchronousDACallbackAdapter, &callback_data);
  return SynchronousDAOperation("DADiskUnmount", &callback_data);
}

// Calls DADiskEject synchronously, returning the result.
bool SynchronousDADiskEject(DADiskRef disk, DADiskEjectOptions options) {
  SynchronousDACallbackData callback_data;
  DADiskEject(disk, options, SynchronousDACallbackAdapter, &callback_data);
  return SynchronousDAOperation("DADiskEject", &callback_data);
}

}  // namespace

void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) {
  base::apple::ScopedCFTypeRef<DASessionRef> session(
      DASessionCreate(/*allocator=*/nullptr));
  if (!session.get()) {
    LOG(ERROR) << "DASessionCreate";
    return;
  }

  base::apple::ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(
      /*allocator=*/nullptr, session.get(), dmg_bsd_device_name.c_str()));
  if (!disk.get()) {
    LOG(ERROR) << "DADiskCreateFromBSDName";
    return;
  }

  // dmg_bsd_device_name may only refer to part of the disk: it may be a
  // single filesystem on a larger disk. Use the "whole disk" object to
  // be able to unmount all mounted filesystems from the disk image, and eject
  // the image. This is harmless if dmg_bsd_device_name already referred to a
  // "whole disk."
  disk.reset(DADiskCopyWholeDisk(disk.get()));
  if (!disk.get()) {
    LOG(ERROR) << "DADiskCopyWholeDisk";
    return;
  }

  base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk.get()));
  if (!media.get()) {
    LOG(ERROR) << "DADiskCopyIOMedia";
    return;
  }

  // Make sure the device is a disk image, and get the path to its disk image
  // file.
  std::string disk_image_path;
  if (!MediaResidesOnDiskImage(media, &disk_image_path)) {
    LOG(ERROR) << "MediaResidesOnDiskImage";
    return;
  }

  // SynchronousDADiskUnmount and SynchronousDADiskEject require that the
  // session be scheduled with the current run loop.
  ScopedDASessionScheduleWithRunLoop session_run_loop(
      session.get(), CFRunLoopGetCurrent(), kCFRunLoopCommonModes);

  // Retry the unmount in a loop to give anything that may have been in use on
  // the disk image (such as crashpad_handler) a chance to exit.
  int tries = 15;
  while (!SynchronousDADiskUnmount(disk.get(), kDADiskUnmountOptionWhole,
                                   --tries == 0)) {
    if (tries == 0) {
      LOG(ERROR) << "SynchronousDADiskUnmount";
      return;
    }
    sleep(1);
  }

  if (!SynchronousDADiskEject(disk.get(), kDADiskEjectOptionDefault)) {
    LOG(ERROR) << "SynchronousDADiskEject";
    return;
  }

  NSURL* disk_image_path_nsurl =
      [NSURL fileURLWithPath:base::SysUTF8ToNSString(disk_image_path)];
  NSError* error = nil;
  if (![NSFileManager.defaultManager trashItemAtURL:disk_image_path_nsurl
                                   resultingItemURL:nil
                                              error:&error]) {
    LOG(ERROR) << base::SysNSStringToUTF8(error.localizedDescription);
    return;
  }
}
