// 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 "components/storage_monitor/storage_monitor_mac.h"

#include <stdint.h>

#include <memory>

#include "base/apple/foundation_util.h"
#include "base/functional/bind.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "components/storage_monitor/media_storage_util.h"
#include "components/storage_monitor/storage_info.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"

namespace storage_monitor {

namespace {

const char16_t kDiskImageModelName[] = u"Disk Image";

std::u16string GetUTF16FromDictionary(CFDictionaryRef dictionary,
                                      CFStringRef key) {
  CFStringRef value =
      base::apple::GetValueFromDictionary<CFStringRef>(dictionary, key);
  if (!value)
    return std::u16string();
  return base::SysCFStringRefToUTF16(value);
}

std::u16string JoinName(const std::u16string& name,
                        const std::u16string& addition) {
  if (addition.empty())
    return name;
  if (name.empty())
    return addition;
  return name + u' ' + addition;
}

StorageInfo::Type GetDeviceType(bool is_removable, bool has_dcim) {
  if (!is_removable)
    return StorageInfo::FIXED_MASS_STORAGE;
  if (has_dcim)
    return StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM;
  return StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM;
}

StorageInfo BuildStorageInfo(
    base::apple::ScopedCFTypeRef<CFDictionaryRef> scoped_dict,
    std::string* bsd_name) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  CFDictionaryRef dict = scoped_dict.get();

  CFStringRef device_bsd_name =
      base::apple::GetValueFromDictionary<CFStringRef>(
          dict, kDADiskDescriptionMediaBSDNameKey);
  if (device_bsd_name && bsd_name)
    *bsd_name = base::SysCFStringRefToUTF8(device_bsd_name);

  CFURLRef url = base::apple::GetValueFromDictionary<CFURLRef>(
      dict, kDADiskDescriptionVolumePathKey);
  base::FilePath location = base::apple::CFURLToFilePath(url);
  CFNumberRef size_number = base::apple::GetValueFromDictionary<CFNumberRef>(
      dict, kDADiskDescriptionMediaSizeKey);
  uint64_t size_in_bytes = 0;
  if (size_number)
    CFNumberGetValue(size_number, kCFNumberLongLongType, &size_in_bytes);

  std::u16string vendor =
      GetUTF16FromDictionary(dict, kDADiskDescriptionDeviceVendorKey);
  std::u16string model =
      GetUTF16FromDictionary(dict, kDADiskDescriptionDeviceModelKey);
  std::u16string label =
      GetUTF16FromDictionary(dict, kDADiskDescriptionVolumeNameKey);

  CFUUIDRef uuid = base::apple::GetValueFromDictionary<CFUUIDRef>(
      dict, kDADiskDescriptionVolumeUUIDKey);
  std::string unique_id;
  if (uuid) {
    base::apple::ScopedCFTypeRef<CFStringRef> uuid_string(
        CFUUIDCreateString(nullptr, uuid));
    if (uuid_string.get())
      unique_id = base::SysCFStringRefToUTF8(uuid_string.get());
  }
  if (unique_id.empty()) {
    std::u16string revision =
        GetUTF16FromDictionary(dict, kDADiskDescriptionDeviceRevisionKey);
    std::u16string unique_id2 = vendor;
    unique_id2 = JoinName(unique_id2, model);
    unique_id2 = JoinName(unique_id2, revision);
    unique_id = base::UTF16ToUTF8(unique_id2);
  }

  CFBooleanRef is_removable_ref =
      base::apple::GetValueFromDictionary<CFBooleanRef>(
          dict, kDADiskDescriptionMediaRemovableKey);
  bool is_removable = is_removable_ref && CFBooleanGetValue(is_removable_ref);
  // Checking for DCIM only matters on removable devices.
  bool has_dcim = is_removable && MediaStorageUtil::HasDcim(location);
  StorageInfo::Type device_type = GetDeviceType(is_removable, has_dcim);
  std::string device_id;
  if (!unique_id.empty())
    device_id = StorageInfo::MakeDeviceId(device_type, unique_id);

  return StorageInfo(device_id, location.value(), label, vendor, model,
                     size_in_bytes);
}

struct EjectDiskOptions {
  std::string bsd_name;
  base::OnceCallback<void(StorageMonitor::EjectStatus)> callback;
  base::apple::ScopedCFTypeRef<DADiskRef> disk;
};

void PostEjectCallback(DADiskRef disk,
                       DADissenterRef dissenter,
                       void* context) {
  std::unique_ptr<EjectDiskOptions> options_deleter(
      static_cast<EjectDiskOptions*>(context));
  if (dissenter) {
    std::move(options_deleter->callback).Run(StorageMonitor::EJECT_IN_USE);
    return;
  }

  std::move(options_deleter->callback).Run(StorageMonitor::EJECT_OK);
}

void PostUnmountCallback(DADiskRef disk,
                         DADissenterRef dissenter,
                         void* context) {
  std::unique_ptr<EjectDiskOptions> options_deleter(
      static_cast<EjectDiskOptions*>(context));
  if (dissenter) {
    std::move(options_deleter->callback).Run(StorageMonitor::EJECT_IN_USE);
    return;
  }

  DADiskEject(options_deleter->disk.get(), kDADiskEjectOptionDefault,
              PostEjectCallback, options_deleter.release());
}

void EjectDisk(EjectDiskOptions* options) {
  DADiskUnmount(options->disk.get(), kDADiskUnmountOptionWhole,
                PostUnmountCallback, options);
}

}  // namespace

StorageMonitorMac::StorageMonitorMac() = default;

StorageMonitorMac::~StorageMonitorMac() {
  if (session_.get()) {
    DASessionUnscheduleFromRunLoop(session_.get(), CFRunLoopGetCurrent(),
                                   kCFRunLoopCommonModes);
  }
}

void StorageMonitorMac::Init() {
  session_.reset(DASessionCreate(nullptr));

  // Register for callbacks for attached, changed, and removed devices.
  // This will send notifications for existing devices too.
  DARegisterDiskAppearedCallback(session_.get(),
                                 kDADiskDescriptionMatchVolumeMountable,
                                 DiskAppearedCallback, this);
  DARegisterDiskDisappearedCallback(session_.get(),
                                    kDADiskDescriptionMatchVolumeMountable,
                                    DiskDisappearedCallback, this);
  DARegisterDiskDescriptionChangedCallback(
      session_.get(), kDADiskDescriptionMatchVolumeMountable,
      kDADiskDescriptionWatchVolumePath, DiskDescriptionChangedCallback, this);

  DASessionScheduleWithRunLoop(session_.get(), CFRunLoopGetCurrent(),
                               kCFRunLoopCommonModes);
}

void StorageMonitorMac::UpdateDisk(UpdateType update_type,
                                   std::string* bsd_name,
                                   const StorageInfo& info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(bsd_name);

  pending_disk_updates_--;
  bool initialization_complete = false;
  if (!IsInitialized() && pending_disk_updates_ == 0)
    initialization_complete = true;

  if (info.device_id().empty() || bsd_name->empty()) {
    if (initialization_complete)
      MarkInitialized();
    return;
  }

  std::map<std::string, StorageInfo>::iterator it =
      disk_info_map_.find(*bsd_name);
  if (it != disk_info_map_.end()) {
    // If an attached notification was previously posted then post a detached
    // notification now. This is used for devices that are being removed or
    // devices that have changed.
    if (ShouldPostNotificationForDisk(it->second)) {
      receiver()->ProcessDetach(it->second.device_id());
    }
  }

  if (update_type == UPDATE_DEVICE_REMOVED) {
    if (it != disk_info_map_.end())
      disk_info_map_.erase(it);
  } else {
    disk_info_map_[*bsd_name] = info;
    if (ShouldPostNotificationForDisk(info))
      receiver()->ProcessAttach(info);
  }

  // We're not really honestly sure we're done, but this looks the best we
  // can do. Any misses should go out through notifications.
  if (initialization_complete)
    MarkInitialized();
}

bool StorageMonitorMac::GetStorageInfoForPath(const base::FilePath& path,
                                              StorageInfo* device_info) const {
  DCHECK(device_info);

  if (!path.IsAbsolute())
    return false;

  base::FilePath current = path;
  const base::FilePath root(base::FilePath::kSeparators);
  while (current != root) {
    StorageInfo info;
    if (FindDiskWithMountPoint(current, &info)) {
      *device_info = info;
      return true;
    }
    current = current.DirName();
  }

  return false;
}

void StorageMonitorMac::EjectDevice(
    const std::string& device_id,
    base::OnceCallback<void(EjectStatus)> callback) {
  StorageInfo::Type type;
  std::string uuid;
  if (!StorageInfo::CrackDeviceId(device_id, &type, &uuid)) {
    std::move(callback).Run(EJECT_FAILURE);
    return;
  }

  std::string bsd_name;
  for (std::map<std::string, StorageInfo>::iterator
      it = disk_info_map_.begin(); it != disk_info_map_.end(); ++it) {
    if (it->second.device_id() == device_id) {
      bsd_name = it->first;
      disk_info_map_.erase(it);
      break;
    }
  }

  if (bsd_name.empty()) {
    std::move(callback).Run(EJECT_NO_SUCH_DEVICE);
    return;
  }

  receiver()->ProcessDetach(device_id);

  base::apple::ScopedCFTypeRef<DADiskRef> disk(
      DADiskCreateFromBSDName(nullptr, session_.get(), bsd_name.c_str()));
  if (!disk.get()) {
    std::move(callback).Run(StorageMonitor::EJECT_FAILURE);
    return;
  }
  // Get the reference to the full disk for ejecting.
  disk.reset(DADiskCopyWholeDisk(disk.get()));
  if (!disk.get()) {
    std::move(callback).Run(StorageMonitor::EJECT_FAILURE);
    return;
  }

  EjectDiskOptions* options = new EjectDiskOptions;
  options->bsd_name = bsd_name;
  options->callback = std::move(callback);
  options->disk = std::move(disk);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(EjectDisk, options));
}

// static
void StorageMonitorMac::DiskAppearedCallback(DADiskRef disk, void* context) {
  StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
  monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_ADDED);
}

// static
void StorageMonitorMac::DiskDisappearedCallback(DADiskRef disk, void* context) {
  StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
  monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_REMOVED);
}

// static
void StorageMonitorMac::DiskDescriptionChangedCallback(DADiskRef disk,
                                                       CFArrayRef keys,
                                                       void *context) {
  StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
  monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_CHANGED);
}

void StorageMonitorMac::GetDiskInfoAndUpdate(
    DADiskRef disk,
    StorageMonitorMac::UpdateType update_type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  pending_disk_updates_++;

  base::apple::ScopedCFTypeRef<CFDictionaryRef> dict(
      DADiskCopyDescription(disk));
  std::string* bsd_name = new std::string;
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&BuildStorageInfo, dict, bsd_name),
      base::BindOnce(&StorageMonitorMac::UpdateDisk,
                     weak_ptr_factory_.GetWeakPtr(), update_type,
                     base::Owned(bsd_name)));
}


bool StorageMonitorMac::ShouldPostNotificationForDisk(
    const StorageInfo& info) const {
  // Only post notifications about disks that have no empty fields and
  // are removable. Also exclude disk images (DMGs).
  return !info.device_id().empty() && !info.location().empty() &&
         info.model_name() != kDiskImageModelName &&
         StorageInfo::IsMassStorageDevice(info.device_id());
}

bool StorageMonitorMac::FindDiskWithMountPoint(
    const base::FilePath& mount_point,
    StorageInfo* info) const {
  for (const auto& disk_info : disk_info_map_) {
    if (disk_info.second.location() == mount_point.value()) {
      *info = disk_info.second;
      return true;
    }
  }
  return false;
}

StorageMonitor* StorageMonitor::CreateInternal() {
  return new StorageMonitorMac();
}

}  // namespace storage_monitor
