1// Copyright (c) 2013 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 "chrome/browser/media_galleries/win/mtp_device_operations_util.h"
6
7#include <portabledevice.h>
8
9#include <algorithm>
10
11#include "base/basictypes.h"
12#include "base/files/file_path.h"
13#include "base/files/file_util.h"
14#include "base/logging.h"
15#include "base/numerics/safe_conversions.h"
16#include "base/strings/string_util.h"
17#include "base/threading/thread_restrictions.h"
18#include "base/time/time.h"
19#include "base/win/scoped_co_mem.h"
20#include "base/win/scoped_propvariant.h"
21#include "chrome/common/chrome_constants.h"
22
23namespace media_transfer_protocol {
24
25namespace {
26
27// On success, returns true and updates |client_info| with a reference to an
28// IPortableDeviceValues interface that holds information about the
29// application that communicates with the device.
30bool GetClientInformation(
31    base::win::ScopedComPtr<IPortableDeviceValues>* client_info) {
32  base::ThreadRestrictions::AssertIOAllowed();
33  DCHECK(client_info);
34  HRESULT hr = client_info->CreateInstance(__uuidof(PortableDeviceValues),
35                                           NULL, CLSCTX_INPROC_SERVER);
36  if (FAILED(hr)) {
37    DPLOG(ERROR) << "Failed to create an instance of IPortableDeviceValues";
38    return false;
39  }
40
41  (*client_info)->SetStringValue(WPD_CLIENT_NAME,
42                                 chrome::kBrowserProcessExecutableName);
43  (*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_MAJOR_VERSION, 0);
44  (*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_MINOR_VERSION, 0);
45  (*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_REVISION, 0);
46  (*client_info)->SetUnsignedIntegerValue(
47      WPD_CLIENT_SECURITY_QUALITY_OF_SERVICE, SECURITY_IMPERSONATION);
48  (*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_DESIRED_ACCESS,
49                                          GENERIC_READ);
50  return true;
51}
52
53// Gets the content interface of the portable |device|. On success, returns
54// the IPortableDeviceContent interface. On failure, returns NULL.
55base::win::ScopedComPtr<IPortableDeviceContent> GetDeviceContent(
56    IPortableDevice* device) {
57  base::ThreadRestrictions::AssertIOAllowed();
58  DCHECK(device);
59  base::win::ScopedComPtr<IPortableDeviceContent> content;
60  if (SUCCEEDED(device->Content(content.Receive())))
61    return content;
62  return base::win::ScopedComPtr<IPortableDeviceContent>();
63}
64
65// On success, returns IEnumPortableDeviceObjectIDs interface to enumerate
66// the device objects. On failure, returns NULL.
67// |parent_id| specifies the parent object identifier.
68base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs> GetDeviceObjectEnumerator(
69    IPortableDevice* device,
70    const base::string16& parent_id) {
71  base::ThreadRestrictions::AssertIOAllowed();
72  DCHECK(device);
73  DCHECK(!parent_id.empty());
74  base::win::ScopedComPtr<IPortableDeviceContent> content =
75      GetDeviceContent(device);
76  if (!content)
77    return base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs>();
78
79  base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs> enum_object_ids;
80  if (SUCCEEDED(content->EnumObjects(0, parent_id.c_str(), NULL,
81                                     enum_object_ids.Receive())))
82    return enum_object_ids;
83  return base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs>();
84}
85
86// Returns whether the object is a directory/folder/album. |properties_values|
87// contains the object property key values.
88bool IsDirectory(IPortableDeviceValues* properties_values) {
89  DCHECK(properties_values);
90  GUID content_type;
91  HRESULT hr = properties_values->GetGuidValue(WPD_OBJECT_CONTENT_TYPE,
92                                               &content_type);
93  if (FAILED(hr))
94    return false;
95  // TODO(kmadhusu): |content_type| can be an image or audio or video or mixed
96  // album. It is not clear whether an album is a collection of physical objects
97  // or virtual objects. Investigate this in detail.
98
99  // The root storage object describes its content type as
100  // WPD_CONTENT_FUNCTIONAL_OBJECT.
101  return (content_type == WPD_CONTENT_TYPE_FOLDER ||
102          content_type == WPD_CONTENT_TYPE_FUNCTIONAL_OBJECT);
103}
104
105// Returns the name of the object from |properties_values|. If the object has
106// no filename, try to use a friendly name instead. e.g. with MTP storage roots.
107base::string16 GetObjectName(IPortableDeviceValues* properties_values) {
108  DCHECK(properties_values);
109  base::string16 result;
110  base::win::ScopedCoMem<base::char16> buffer;
111  HRESULT hr = properties_values->GetStringValue(WPD_OBJECT_ORIGINAL_FILE_NAME,
112                                                 &buffer);
113  if (FAILED(hr))
114    hr = properties_values->GetStringValue(WPD_OBJECT_NAME, &buffer);
115  if (SUCCEEDED(hr))
116    result.assign(buffer);
117  return result;
118}
119
120// Gets the last modified time of the object from the property key values
121// specified by the |properties_values|. On success, fills in
122// |last_modified_time|.
123void GetLastModifiedTime(IPortableDeviceValues* properties_values,
124                         base::Time* last_modified_time) {
125  DCHECK(properties_values);
126  DCHECK(last_modified_time);
127  base::win::ScopedPropVariant last_modified_date;
128  HRESULT hr = properties_values->GetValue(WPD_OBJECT_DATE_MODIFIED,
129                                           last_modified_date.Receive());
130  if (FAILED(hr))
131    return;
132
133  // Some PTP devices don't provide an mtime. Try using the ctime instead.
134  if (last_modified_date.get().vt != VT_DATE) {
135    last_modified_date.Reset();
136    HRESULT hr = properties_values->GetValue(WPD_OBJECT_DATE_CREATED,
137                                             last_modified_date.Receive());
138    if (FAILED(hr))
139      return;
140  }
141
142  SYSTEMTIME system_time;
143  FILETIME file_time;
144  if (last_modified_date.get().vt == VT_DATE &&
145      VariantTimeToSystemTime(last_modified_date.get().date, &system_time) &&
146      SystemTimeToFileTime(&system_time, &file_time)) {
147    *last_modified_time = base::Time::FromFileTime(file_time);
148  }
149}
150
151// Gets the size of the file object in bytes from the property key values
152// specified by the |properties_values|. On failure, return -1.
153int64 GetObjectSize(IPortableDeviceValues* properties_values) {
154  DCHECK(properties_values);
155  ULONGLONG actual_size;
156  HRESULT hr = properties_values->GetUnsignedLargeIntegerValue(WPD_OBJECT_SIZE,
157                                                               &actual_size);
158  bool success = SUCCEEDED(hr) && (actual_size <= kint64max);
159  return success ? static_cast<int64>(actual_size) : -1;
160}
161
162// Gets the details of the object specified by the |object_id| given the media
163// transfer protocol |device|. On success, returns true and fills in |name|,
164// |is_directory|, |size|. |last_modified_time| will be filled in if possible,
165// but failure to get it doesn't prevent success.
166bool GetObjectDetails(IPortableDevice* device,
167                      const base::string16 object_id,
168                      base::string16* name,
169                      bool* is_directory,
170                      int64* size,
171                      base::Time* last_modified_time) {
172  base::ThreadRestrictions::AssertIOAllowed();
173  DCHECK(device);
174  DCHECK(!object_id.empty());
175  DCHECK(name);
176  DCHECK(is_directory);
177  DCHECK(size);
178  DCHECK(last_modified_time);
179  base::win::ScopedComPtr<IPortableDeviceContent> content =
180      GetDeviceContent(device);
181  if (!content)
182    return false;
183
184  base::win::ScopedComPtr<IPortableDeviceProperties> properties;
185  HRESULT hr = content->Properties(properties.Receive());
186  if (FAILED(hr))
187    return false;
188
189  base::win::ScopedComPtr<IPortableDeviceKeyCollection> properties_to_read;
190  hr = properties_to_read.CreateInstance(__uuidof(PortableDeviceKeyCollection),
191                                         NULL,
192                                         CLSCTX_INPROC_SERVER);
193  if (FAILED(hr))
194    return false;
195
196  if (FAILED(properties_to_read->Add(WPD_OBJECT_CONTENT_TYPE)) ||
197      FAILED(properties_to_read->Add(WPD_OBJECT_FORMAT)) ||
198      FAILED(properties_to_read->Add(WPD_OBJECT_ORIGINAL_FILE_NAME)) ||
199      FAILED(properties_to_read->Add(WPD_OBJECT_NAME)) ||
200      FAILED(properties_to_read->Add(WPD_OBJECT_DATE_MODIFIED)) ||
201      FAILED(properties_to_read->Add(WPD_OBJECT_DATE_CREATED)) ||
202      FAILED(properties_to_read->Add(WPD_OBJECT_SIZE)))
203    return false;
204
205  base::win::ScopedComPtr<IPortableDeviceValues> properties_values;
206  hr = properties->GetValues(object_id.c_str(),
207                             properties_to_read.get(),
208                             properties_values.Receive());
209  if (FAILED(hr))
210    return false;
211
212  *is_directory = IsDirectory(properties_values.get());
213  *name = GetObjectName(properties_values.get());
214  if (name->empty())
215    return false;
216
217  if (*is_directory) {
218    // Directory entry does not have size and last modified date property key
219    // values.
220    *size = 0;
221    *last_modified_time = base::Time();
222    return true;
223  }
224
225  // Try to get the last modified time, but don't fail if we can't.
226  GetLastModifiedTime(properties_values.get(), last_modified_time);
227
228  int64 object_size = GetObjectSize(properties_values.get());
229  if (object_size < 0)
230    return false;
231  *size = object_size;
232  return true;
233}
234
235// Creates an MTP device object entry for the given |device| and |object_id|.
236// On success, returns true and fills in |entry|.
237MTPDeviceObjectEntry GetMTPDeviceObjectEntry(IPortableDevice* device,
238                                             const base::string16& object_id) {
239  base::ThreadRestrictions::AssertIOAllowed();
240  DCHECK(device);
241  DCHECK(!object_id.empty());
242  base::string16 name;
243  bool is_directory;
244  int64 size;
245  base::Time last_modified_time;
246  MTPDeviceObjectEntry entry;
247  if (GetObjectDetails(device, object_id, &name, &is_directory, &size,
248                       &last_modified_time)) {
249    entry = MTPDeviceObjectEntry(object_id, name, is_directory, size,
250                                 last_modified_time);
251  }
252  return entry;
253}
254
255// Gets the entries of the directory specified by |directory_object_id| from
256// the given MTP |device|. To request a specific object entry, put the object
257// name in |object_name|. Leave |object_name| blank to request all entries. On
258// success returns true and set |object_entries|.
259bool GetMTPDeviceObjectEntries(IPortableDevice* device,
260                               const base::string16& directory_object_id,
261                               const base::string16& object_name,
262                               MTPDeviceObjectEntries* object_entries) {
263  base::ThreadRestrictions::AssertIOAllowed();
264  DCHECK(device);
265  DCHECK(!directory_object_id.empty());
266  DCHECK(object_entries);
267  base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs> enum_object_ids =
268      GetDeviceObjectEnumerator(device, directory_object_id);
269  if (!enum_object_ids)
270    return false;
271
272  // Loop calling Next() while S_OK is being returned.
273  const DWORD num_objects_to_request = 10;
274  const bool get_all_entries = object_name.empty();
275  for (HRESULT hr = S_OK; hr == S_OK;) {
276    DWORD num_objects_fetched = 0;
277    scoped_ptr<base::char16*[]> object_ids(
278        new base::char16*[num_objects_to_request]);
279    hr = enum_object_ids->Next(num_objects_to_request,
280                               object_ids.get(),
281                               &num_objects_fetched);
282    for (DWORD i = 0; i < num_objects_fetched; ++i) {
283      MTPDeviceObjectEntry entry =
284          GetMTPDeviceObjectEntry(device, object_ids[i]);
285      if (entry.object_id.empty())
286        continue;
287      if (get_all_entries) {
288        object_entries->push_back(entry);
289      } else if (entry.name == object_name) {
290        object_entries->push_back(entry);  // Object entry found.
291        break;
292      }
293    }
294    for (DWORD i = 0; i < num_objects_fetched; ++i)
295      CoTaskMemFree(object_ids[i]);
296  }
297  return true;
298}
299
300}  // namespace
301
302base::win::ScopedComPtr<IPortableDevice> OpenDevice(
303    const base::string16& pnp_device_id) {
304  base::ThreadRestrictions::AssertIOAllowed();
305  DCHECK(!pnp_device_id.empty());
306  base::win::ScopedComPtr<IPortableDeviceValues> client_info;
307  if (!GetClientInformation(&client_info))
308    return base::win::ScopedComPtr<IPortableDevice>();
309  base::win::ScopedComPtr<IPortableDevice> device;
310  HRESULT hr = device.CreateInstance(__uuidof(PortableDevice), NULL,
311                                     CLSCTX_INPROC_SERVER);
312  if (FAILED(hr))
313    return base::win::ScopedComPtr<IPortableDevice>();
314
315  hr = device->Open(pnp_device_id.c_str(), client_info.get());
316  if (SUCCEEDED(hr))
317    return device;
318  if (hr == E_ACCESSDENIED)
319    DPLOG(ERROR) << "Access denied to open the device";
320  return base::win::ScopedComPtr<IPortableDevice>();
321}
322
323base::File::Error GetFileEntryInfo(
324    IPortableDevice* device,
325    const base::string16& object_id,
326    base::File::Info* file_entry_info) {
327  DCHECK(device);
328  DCHECK(!object_id.empty());
329  DCHECK(file_entry_info);
330  MTPDeviceObjectEntry entry = GetMTPDeviceObjectEntry(device, object_id);
331  if (entry.object_id.empty())
332    return base::File::FILE_ERROR_NOT_FOUND;
333
334  file_entry_info->size = entry.size;
335  file_entry_info->is_directory = entry.is_directory;
336  file_entry_info->is_symbolic_link = false;
337  file_entry_info->last_modified = entry.last_modified_time;
338  file_entry_info->last_accessed = entry.last_modified_time;
339  file_entry_info->creation_time = base::Time();
340  return base::File::FILE_OK;
341}
342
343bool GetDirectoryEntries(IPortableDevice* device,
344                         const base::string16& directory_object_id,
345                         MTPDeviceObjectEntries* object_entries) {
346  return GetMTPDeviceObjectEntries(device, directory_object_id,
347                                   base::string16(), object_entries);
348}
349
350HRESULT GetFileStreamForObject(IPortableDevice* device,
351                               const base::string16& file_object_id,
352                               IStream** file_stream,
353                               DWORD* optimal_transfer_size) {
354  base::ThreadRestrictions::AssertIOAllowed();
355  DCHECK(device);
356  DCHECK(!file_object_id.empty());
357  base::win::ScopedComPtr<IPortableDeviceContent> content =
358      GetDeviceContent(device);
359  if (!content)
360    return E_FAIL;
361
362  base::win::ScopedComPtr<IPortableDeviceResources> resources;
363  HRESULT hr = content->Transfer(resources.Receive());
364  if (FAILED(hr))
365    return hr;
366  return resources->GetStream(file_object_id.c_str(), WPD_RESOURCE_DEFAULT,
367                              STGM_READ, optimal_transfer_size,
368                              file_stream);
369}
370
371DWORD CopyDataChunkToLocalFile(IStream* stream,
372                               const base::FilePath& local_path,
373                               size_t optimal_transfer_size) {
374  base::ThreadRestrictions::AssertIOAllowed();
375  DCHECK(stream);
376  DCHECK(!local_path.empty());
377  if (optimal_transfer_size == 0U)
378    return 0U;
379  DWORD bytes_read = 0;
380  std::string buffer;
381  HRESULT hr = stream->Read(WriteInto(&buffer, optimal_transfer_size + 1),
382                            optimal_transfer_size, &bytes_read);
383  // IStream::Read() returns S_FALSE when the actual number of bytes read from
384  // the stream object is less than the number of bytes requested (aka
385  // |optimal_transfer_size|). This indicates the end of the stream has been
386  // reached.
387  if (FAILED(hr))
388    return 0U;
389  DCHECK_GT(bytes_read, 0U);
390  CHECK_LE(bytes_read, buffer.length());
391  int data_len =
392      base::checked_cast<int>(
393          std::min(bytes_read,
394                   base::checked_cast<DWORD>(buffer.length())));
395  if (base::AppendToFile(local_path, buffer.c_str(), data_len) != data_len)
396    return 0U;
397  return data_len;
398}
399
400base::string16 GetObjectIdFromName(IPortableDevice* device,
401                                   const base::string16& parent_id,
402                                   const base::string16& object_name) {
403  MTPDeviceObjectEntries object_entries;
404  if (!GetMTPDeviceObjectEntries(device, parent_id, object_name,
405                                 &object_entries) ||
406      object_entries.empty())
407    return base::string16();
408  // TODO(thestig): This DCHECK can fail. Multiple MTP objects can have
409  // the same name. Handle the situation gracefully. Refer to crbug.com/169930
410  // for more details.
411  DCHECK_EQ(1U, object_entries.size());
412  return object_entries[0].object_id;
413}
414
415}  // namespace media_transfer_protocol
416