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