1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/storage_monitor/storage_monitor_mac.h"
6
7#include <stdint.h>
8
9#include "base/bind.h"
10#include "base/mac/foundation_util.h"
11#include "base/mac/mac_util.h"
12#include "base/strings/sys_string_conversions.h"
13#include "base/strings/utf_string_conversions.h"
14#include "base/task/task_traits.h"
15#include "base/task/thread_pool.h"
16#include "base/threading/scoped_blocking_call.h"
17#include "components/storage_monitor/image_capture_device_manager.h"
18#include "components/storage_monitor/media_storage_util.h"
19#include "components/storage_monitor/storage_info.h"
20#include "content/public/browser/browser_task_traits.h"
21#include "content/public/browser/browser_thread.h"
22
23namespace storage_monitor {
24
25namespace {
26
27const char kDiskImageModelName[] = "Disk Image";
28
29base::string16 GetUTF16FromDictionary(CFDictionaryRef dictionary,
30                                      CFStringRef key) {
31  CFStringRef value =
32      base::mac::GetValueFromDictionary<CFStringRef>(dictionary, key);
33  if (!value)
34    return base::string16();
35  return base::SysCFStringRefToUTF16(value);
36}
37
38base::string16 JoinName(const base::string16& name,
39                        const base::string16& addition) {
40  if (addition.empty())
41    return name;
42  if (name.empty())
43    return addition;
44  return name + static_cast<base::char16>(' ') + addition;
45}
46
47StorageInfo::Type GetDeviceType(bool is_removable, bool has_dcim) {
48  if (!is_removable)
49    return StorageInfo::FIXED_MASS_STORAGE;
50  if (has_dcim)
51    return StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM;
52  return StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM;
53}
54
55StorageInfo BuildStorageInfo(
56    CFDictionaryRef dict, std::string* bsd_name) {
57  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
58                                                base::BlockingType::MAY_BLOCK);
59
60  CFStringRef device_bsd_name = base::mac::GetValueFromDictionary<CFStringRef>(
61      dict, kDADiskDescriptionMediaBSDNameKey);
62  if (device_bsd_name && bsd_name)
63    *bsd_name = base::SysCFStringRefToUTF8(device_bsd_name);
64
65  CFURLRef url = base::mac::GetValueFromDictionary<CFURLRef>(
66      dict, kDADiskDescriptionVolumePathKey);
67  NSURL* nsurl = base::mac::CFToNSCast(url);
68  base::FilePath location = base::mac::NSStringToFilePath([nsurl path]);
69  CFNumberRef size_number =
70      base::mac::GetValueFromDictionary<CFNumberRef>(
71          dict, kDADiskDescriptionMediaSizeKey);
72  uint64_t size_in_bytes = 0;
73  if (size_number)
74    CFNumberGetValue(size_number, kCFNumberLongLongType, &size_in_bytes);
75
76  base::string16 vendor = GetUTF16FromDictionary(
77      dict, kDADiskDescriptionDeviceVendorKey);
78  base::string16 model = GetUTF16FromDictionary(
79      dict, kDADiskDescriptionDeviceModelKey);
80  base::string16 label = GetUTF16FromDictionary(
81      dict, kDADiskDescriptionVolumeNameKey);
82
83  CFUUIDRef uuid = base::mac::GetValueFromDictionary<CFUUIDRef>(
84      dict, kDADiskDescriptionVolumeUUIDKey);
85  std::string unique_id;
86  if (uuid) {
87    base::ScopedCFTypeRef<CFStringRef> uuid_string(
88        CFUUIDCreateString(NULL, uuid));
89    if (uuid_string.get())
90      unique_id = base::SysCFStringRefToUTF8(uuid_string);
91  }
92  if (unique_id.empty()) {
93    base::string16 revision = GetUTF16FromDictionary(
94        dict, kDADiskDescriptionDeviceRevisionKey);
95    base::string16 unique_id2 = vendor;
96    unique_id2 = JoinName(unique_id2, model);
97    unique_id2 = JoinName(unique_id2, revision);
98    unique_id = base::UTF16ToUTF8(unique_id2);
99  }
100
101  CFBooleanRef is_removable_ref =
102      base::mac::GetValueFromDictionary<CFBooleanRef>(
103          dict, kDADiskDescriptionMediaRemovableKey);
104  bool is_removable = is_removable_ref && CFBooleanGetValue(is_removable_ref);
105  // Checking for DCIM only matters on removable devices.
106  bool has_dcim = is_removable && MediaStorageUtil::HasDcim(location);
107  StorageInfo::Type device_type = GetDeviceType(is_removable, has_dcim);
108  std::string device_id;
109  if (!unique_id.empty())
110    device_id = StorageInfo::MakeDeviceId(device_type, unique_id);
111
112  return StorageInfo(device_id, location.value(), label, vendor, model,
113                     size_in_bytes);
114}
115
116struct EjectDiskOptions {
117  std::string bsd_name;
118  base::OnceCallback<void(StorageMonitor::EjectStatus)> callback;
119  base::ScopedCFTypeRef<DADiskRef> disk;
120};
121
122void PostEjectCallback(DADiskRef disk,
123                       DADissenterRef dissenter,
124                       void* context) {
125  std::unique_ptr<EjectDiskOptions> options_deleter(
126      static_cast<EjectDiskOptions*>(context));
127  if (dissenter) {
128    std::move(options_deleter->callback).Run(StorageMonitor::EJECT_IN_USE);
129    return;
130  }
131
132  std::move(options_deleter->callback).Run(StorageMonitor::EJECT_OK);
133}
134
135void PostUnmountCallback(DADiskRef disk,
136                         DADissenterRef dissenter,
137                         void* context) {
138  std::unique_ptr<EjectDiskOptions> options_deleter(
139      static_cast<EjectDiskOptions*>(context));
140  if (dissenter) {
141    std::move(options_deleter->callback).Run(StorageMonitor::EJECT_IN_USE);
142    return;
143  }
144
145  DADiskEject(options_deleter->disk.get(), kDADiskEjectOptionDefault,
146              PostEjectCallback, options_deleter.release());
147}
148
149void EjectDisk(EjectDiskOptions* options) {
150  DADiskUnmount(options->disk.get(), kDADiskUnmountOptionWhole,
151                PostUnmountCallback, options);
152}
153
154}  // namespace
155
156StorageMonitorMac::StorageMonitorMac() : pending_disk_updates_(0) {
157}
158
159StorageMonitorMac::~StorageMonitorMac() {
160  if (session_.get()) {
161    DASessionUnscheduleFromRunLoop(
162        session_, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
163  }
164}
165
166void StorageMonitorMac::Init() {
167  session_.reset(DASessionCreate(NULL));
168
169  // Register for callbacks for attached, changed, and removed devices.
170  // This will send notifications for existing devices too.
171  DARegisterDiskAppearedCallback(
172      session_,
173      kDADiskDescriptionMatchVolumeMountable,
174      DiskAppearedCallback,
175      this);
176  DARegisterDiskDisappearedCallback(
177      session_,
178      kDADiskDescriptionMatchVolumeMountable,
179      DiskDisappearedCallback,
180      this);
181  DARegisterDiskDescriptionChangedCallback(
182      session_,
183      kDADiskDescriptionMatchVolumeMountable,
184      kDADiskDescriptionWatchVolumePath,
185      DiskDescriptionChangedCallback,
186      this);
187
188  DASessionScheduleWithRunLoop(
189      session_, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
190
191  image_capture_device_manager_.reset(new ImageCaptureDeviceManager);
192  image_capture_device_manager_->SetNotifications(receiver());
193}
194
195void StorageMonitorMac::UpdateDisk(UpdateType update_type,
196                                   std::string* bsd_name,
197                                   const StorageInfo& info) {
198  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
199  DCHECK(bsd_name);
200
201  pending_disk_updates_--;
202  bool initialization_complete = false;
203  if (!IsInitialized() && pending_disk_updates_ == 0)
204    initialization_complete = true;
205
206  if (info.device_id().empty() || bsd_name->empty()) {
207    if (initialization_complete)
208      MarkInitialized();
209    return;
210  }
211
212  std::map<std::string, StorageInfo>::iterator it =
213      disk_info_map_.find(*bsd_name);
214  if (it != disk_info_map_.end()) {
215    // If an attached notification was previously posted then post a detached
216    // notification now. This is used for devices that are being removed or
217    // devices that have changed.
218    if (ShouldPostNotificationForDisk(it->second)) {
219      receiver()->ProcessDetach(it->second.device_id());
220    }
221  }
222
223  if (update_type == UPDATE_DEVICE_REMOVED) {
224    if (it != disk_info_map_.end())
225      disk_info_map_.erase(it);
226  } else {
227    disk_info_map_[*bsd_name] = info;
228    if (ShouldPostNotificationForDisk(info))
229      receiver()->ProcessAttach(info);
230  }
231
232  // We're not really honestly sure we're done, but this looks the best we
233  // can do. Any misses should go out through notifications.
234  if (initialization_complete)
235    MarkInitialized();
236}
237
238bool StorageMonitorMac::GetStorageInfoForPath(const base::FilePath& path,
239                                              StorageInfo* device_info) const {
240  DCHECK(device_info);
241
242  if (!path.IsAbsolute())
243    return false;
244
245  base::FilePath current = path;
246  const base::FilePath root(base::FilePath::kSeparators);
247  while (current != root) {
248    StorageInfo info;
249    if (FindDiskWithMountPoint(current, &info)) {
250      *device_info = info;
251      return true;
252    }
253    current = current.DirName();
254  }
255
256  return false;
257}
258
259void StorageMonitorMac::EjectDevice(
260    const std::string& device_id,
261    base::OnceCallback<void(EjectStatus)> callback) {
262  StorageInfo::Type type;
263  std::string uuid;
264  if (!StorageInfo::CrackDeviceId(device_id, &type, &uuid)) {
265    std::move(callback).Run(EJECT_FAILURE);
266    return;
267  }
268
269  if (type == StorageInfo::MAC_IMAGE_CAPTURE &&
270      image_capture_device_manager_.get()) {
271    image_capture_device_manager_->EjectDevice(uuid, std::move(callback));
272    return;
273  }
274
275  std::string bsd_name;
276  for (std::map<std::string, StorageInfo>::iterator
277      it = disk_info_map_.begin(); it != disk_info_map_.end(); ++it) {
278    if (it->second.device_id() == device_id) {
279      bsd_name = it->first;
280      disk_info_map_.erase(it);
281      break;
282    }
283  }
284
285  if (bsd_name.empty()) {
286    std::move(callback).Run(EJECT_NO_SUCH_DEVICE);
287    return;
288  }
289
290  receiver()->ProcessDetach(device_id);
291
292  base::ScopedCFTypeRef<DADiskRef> disk(
293      DADiskCreateFromBSDName(NULL, session_, bsd_name.c_str()));
294  if (!disk.get()) {
295    std::move(callback).Run(StorageMonitor::EJECT_FAILURE);
296    return;
297  }
298  // Get the reference to the full disk for ejecting.
299  disk.reset(DADiskCopyWholeDisk(disk));
300  if (!disk.get()) {
301    std::move(callback).Run(StorageMonitor::EJECT_FAILURE);
302    return;
303  }
304
305  EjectDiskOptions* options = new EjectDiskOptions;
306  options->bsd_name = bsd_name;
307  options->callback = std::move(callback);
308  options->disk = std::move(disk);
309  content::GetUIThreadTaskRunner({})->PostTask(
310      FROM_HERE, base::BindOnce(EjectDisk, options));
311}
312
313// static
314void StorageMonitorMac::DiskAppearedCallback(DADiskRef disk, void* context) {
315  StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
316  monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_ADDED);
317}
318
319// static
320void StorageMonitorMac::DiskDisappearedCallback(DADiskRef disk, void* context) {
321  StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
322  monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_REMOVED);
323}
324
325// static
326void StorageMonitorMac::DiskDescriptionChangedCallback(DADiskRef disk,
327                                                       CFArrayRef keys,
328                                                       void *context) {
329  StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
330  monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_CHANGED);
331}
332
333void StorageMonitorMac::GetDiskInfoAndUpdate(
334    DADiskRef disk,
335    StorageMonitorMac::UpdateType update_type) {
336  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
337
338  pending_disk_updates_++;
339
340  base::ScopedCFTypeRef<CFDictionaryRef> dict(DADiskCopyDescription(disk));
341  std::string* bsd_name = new std::string;
342  base::ThreadPool::PostTaskAndReplyWithResult(
343      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
344      base::BindOnce(&BuildStorageInfo, dict, bsd_name),
345      base::BindOnce(&StorageMonitorMac::UpdateDisk, AsWeakPtr(), update_type,
346                     base::Owned(bsd_name)));
347}
348
349
350bool StorageMonitorMac::ShouldPostNotificationForDisk(
351    const StorageInfo& info) const {
352  // Only post notifications about disks that have no empty fields and
353  // are removable. Also exclude disk images (DMGs).
354  return !info.device_id().empty() &&
355         !info.location().empty() &&
356         info.model_name() != base::ASCIIToUTF16(kDiskImageModelName) &&
357         StorageInfo::IsMassStorageDevice(info.device_id());
358}
359
360bool StorageMonitorMac::FindDiskWithMountPoint(
361    const base::FilePath& mount_point,
362    StorageInfo* info) const {
363  for (std::map<std::string, StorageInfo>::const_iterator
364      it = disk_info_map_.begin(); it != disk_info_map_.end(); ++it) {
365    if (it->second.location() == mount_point.value()) {
366      *info = it->second;
367      return true;
368    }
369  }
370  return false;
371}
372
373StorageMonitor* StorageMonitor::CreateInternal() {
374  return new StorageMonitorMac();
375}
376
377}  // namespace storage_monitor
378