blob: 8424183840c88a0abc5a6d0e2acc93674e2fbcda [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media_galleries/win/mtp_device_operations_util.h"
#include <portabledevice.h>
#include <algorithm>
#include "base/basictypes.h"
#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/safe_numerics.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/scoped_propvariant.h"
#include "chrome/browser/storage_monitor/removable_device_constants.h"
#include "chrome/common/chrome_constants.h"
#include "content/public/browser/browser_thread.h"
namespace media_transfer_protocol {
namespace {
// On success, returns true and updates |client_info| with a reference to an
// IPortableDeviceValues interface that holds information about the
// application that communicates with the device.
bool GetClientInformation(
base::win::ScopedComPtr<IPortableDeviceValues>* client_info) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(client_info);
HRESULT hr = client_info->CreateInstance(__uuidof(PortableDeviceValues),
NULL, CLSCTX_INPROC_SERVER);
if (FAILED(hr)) {
DPLOG(ERROR) << "Failed to create an instance of IPortableDeviceValues";
return false;
}
(*client_info)->SetStringValue(WPD_CLIENT_NAME,
chrome::kBrowserProcessExecutableName);
(*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_MAJOR_VERSION, 0);
(*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_MINOR_VERSION, 0);
(*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_REVISION, 0);
(*client_info)->SetUnsignedIntegerValue(
WPD_CLIENT_SECURITY_QUALITY_OF_SERVICE, SECURITY_IMPERSONATION);
(*client_info)->SetUnsignedIntegerValue(WPD_CLIENT_DESIRED_ACCESS,
GENERIC_READ);
return true;
}
// Gets the content interface of the portable |device|. On success, returns
// the IPortableDeviceContent interface. On failure, returns NULL.
base::win::ScopedComPtr<IPortableDeviceContent> GetDeviceContent(
IPortableDevice* device) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(device);
base::win::ScopedComPtr<IPortableDeviceContent> content;
if (SUCCEEDED(device->Content(content.Receive())))
return content;
return base::win::ScopedComPtr<IPortableDeviceContent>();
}
// On success, returns IEnumPortableDeviceObjectIDs interface to enumerate
// the device objects. On failure, returns NULL.
// |parent_id| specifies the parent object identifier.
base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs> GetDeviceObjectEnumerator(
IPortableDevice* device,
const base::string16& parent_id) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(device);
DCHECK(!parent_id.empty());
base::win::ScopedComPtr<IPortableDeviceContent> content =
GetDeviceContent(device);
if (!content)
return base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs>();
base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs> enum_object_ids;
if (SUCCEEDED(content->EnumObjects(0, parent_id.c_str(), NULL,
enum_object_ids.Receive())))
return enum_object_ids;
return base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs>();
}
// Returns whether the object is a directory/folder/album. |properties_values|
// contains the object property key values.
bool IsDirectory(IPortableDeviceValues* properties_values) {
DCHECK(properties_values);
GUID content_type;
HRESULT hr = properties_values->GetGuidValue(WPD_OBJECT_CONTENT_TYPE,
&content_type);
if (FAILED(hr))
return false;
// TODO(kmadhusu): |content_type| can be an image or audio or video or mixed
// album. It is not clear whether an album is a collection of physical objects
// or virtual objects. Investigate this in detail.
// The root storage object describes its content type as
// WPD_CONTENT_FUNCTIONAL_OBJECT.
return (content_type == WPD_CONTENT_TYPE_FOLDER ||
content_type == WPD_CONTENT_TYPE_FUNCTIONAL_OBJECT);
}
// Returns the friendly name of the object from the property key values
// specified by the |properties_values|.
base::string16 GetObjectName(IPortableDeviceValues* properties_values,
bool is_directory) {
DCHECK(properties_values);
base::win::ScopedCoMem<char16> buffer;
REFPROPERTYKEY key =
is_directory ? WPD_OBJECT_NAME : WPD_OBJECT_ORIGINAL_FILE_NAME;
HRESULT hr = properties_values->GetStringValue(key, &buffer);
base::string16 result;
if (SUCCEEDED(hr))
result.assign(buffer);
return result;
}
// Gets the last modified time of the object from the property key values
// specified by the |properties_values|. On success, returns true and fills in
// |last_modified_time|.
bool GetLastModifiedTime(IPortableDeviceValues* properties_values,
base::Time* last_modified_time) {
DCHECK(properties_values);
DCHECK(last_modified_time);
base::win::ScopedPropVariant last_modified_date;
HRESULT hr = properties_values->GetValue(WPD_OBJECT_DATE_MODIFIED,
last_modified_date.Receive());
if (FAILED(hr))
return false;
bool last_modified_time_set = (last_modified_date.get().vt == VT_DATE);
if (last_modified_time_set) {
SYSTEMTIME system_time;
FILETIME file_time;
if (VariantTimeToSystemTime(last_modified_date.get().date, &system_time) &&
SystemTimeToFileTime(&system_time, &file_time)) {
*last_modified_time = base::Time::FromFileTime(file_time);
} else {
last_modified_time_set = false;
}
}
return last_modified_time_set;
}
// Gets the size of the file object in bytes from the property key values
// specified by the |properties_values|. On success, returns true and fills
// in |size|.
bool GetObjectSize(IPortableDeviceValues* properties_values, int64* size) {
DCHECK(properties_values);
DCHECK(size);
ULONGLONG actual_size;
HRESULT hr = properties_values->GetUnsignedLargeIntegerValue(WPD_OBJECT_SIZE,
&actual_size);
bool success = SUCCEEDED(hr) && (actual_size <= kint64max);
if (success)
*size = static_cast<int64>(actual_size);
return success;
}
// Gets the details of the object specified by the |object_id| given the media
// transfer protocol |device|. On success, returns true and fills in |name|,
// |is_directory|, |size| and |last_modified_time|.
bool GetObjectDetails(IPortableDevice* device,
const base::string16 object_id,
base::string16* name,
bool* is_directory,
int64* size,
base::Time* last_modified_time) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(device);
DCHECK(!object_id.empty());
DCHECK(name);
DCHECK(is_directory);
DCHECK(size);
DCHECK(last_modified_time);
base::win::ScopedComPtr<IPortableDeviceContent> content =
GetDeviceContent(device);
if (!content)
return false;
base::win::ScopedComPtr<IPortableDeviceProperties> properties;
HRESULT hr = content->Properties(properties.Receive());
if (FAILED(hr))
return false;
base::win::ScopedComPtr<IPortableDeviceKeyCollection> properties_to_read;
hr = properties_to_read.CreateInstance(__uuidof(PortableDeviceKeyCollection),
NULL,
CLSCTX_INPROC_SERVER);
if (FAILED(hr))
return false;
if (FAILED(properties_to_read->Add(WPD_OBJECT_CONTENT_TYPE)) ||
FAILED(properties_to_read->Add(WPD_OBJECT_FORMAT)) ||
FAILED(properties_to_read->Add(WPD_OBJECT_ORIGINAL_FILE_NAME)) ||
FAILED(properties_to_read->Add(WPD_OBJECT_NAME)) ||
FAILED(properties_to_read->Add(WPD_OBJECT_DATE_MODIFIED)) ||
FAILED(properties_to_read->Add(WPD_OBJECT_SIZE)))
return false;
base::win::ScopedComPtr<IPortableDeviceValues> properties_values;
hr = properties->GetValues(object_id.c_str(),
properties_to_read.get(),
properties_values.Receive());
if (FAILED(hr))
return false;
*is_directory = IsDirectory(properties_values.get());
*name = GetObjectName(properties_values.get(), *is_directory);
if (name->empty())
return false;
if (*is_directory) {
// Directory entry does not have size and last modified date property key
// values.
*size = 0;
*last_modified_time = base::Time();
return true;
}
return (GetObjectSize(properties_values.get(), size) &&
GetLastModifiedTime(properties_values.get(), last_modified_time));
}
// Creates an MTP device object entry for the given |device| and |object_id|.
// On success, returns true and fills in |entry|.
bool GetMTPDeviceObjectEntry(IPortableDevice* device,
const base::string16& object_id,
MTPDeviceObjectEntry* entry) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(device);
DCHECK(!object_id.empty());
DCHECK(entry);
base::string16 name;
bool is_directory;
int64 size;
base::Time last_modified_time;
if (!GetObjectDetails(device, object_id, &name, &is_directory, &size,
&last_modified_time))
return false;
*entry = MTPDeviceObjectEntry(object_id, name, is_directory, size,
last_modified_time);
return true;
}
// Gets the entries of the directory specified by |directory_object_id| from
// the given MTP |device|. Set |get_all_entries| to true to get all the entries
// of the directory. To request a specific object entry, put the object name in
// |object_name|. Leave |object_name| blank to request all entries. On
// success returns true and set |object_entries|.
bool GetMTPDeviceObjectEntries(IPortableDevice* device,
const base::string16& directory_object_id,
const base::string16& object_name,
MTPDeviceObjectEntries* object_entries) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(device);
DCHECK(!directory_object_id.empty());
DCHECK(object_entries);
base::win::ScopedComPtr<IEnumPortableDeviceObjectIDs> enum_object_ids =
GetDeviceObjectEnumerator(device, directory_object_id);
if (!enum_object_ids)
return false;
// Loop calling Next() while S_OK is being returned.
const DWORD num_objects_to_request = 10;
const bool get_all_entries = object_name.empty();
for (HRESULT hr = S_OK; hr == S_OK;) {
DWORD num_objects_fetched = 0;
scoped_ptr<char16*[]> object_ids(new char16*[num_objects_to_request]);
hr = enum_object_ids->Next(num_objects_to_request,
object_ids.get(),
&num_objects_fetched);
for (DWORD index = 0; index < num_objects_fetched; ++index) {
MTPDeviceObjectEntry entry;
if (GetMTPDeviceObjectEntry(device,
object_ids[index],
&entry)) {
if (get_all_entries) {
object_entries->push_back(entry);
} else if (entry.name == object_name) {
object_entries->push_back(entry); // Object entry found.
break;
}
}
}
for (DWORD index = 0; index < num_objects_fetched; ++index)
CoTaskMemFree(object_ids[index]);
}
return true;
}
} // namespace
base::win::ScopedComPtr<IPortableDevice> OpenDevice(
const base::string16& pnp_device_id) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(!pnp_device_id.empty());
base::win::ScopedComPtr<IPortableDeviceValues> client_info;
if (!GetClientInformation(&client_info))
return base::win::ScopedComPtr<IPortableDevice>();
base::win::ScopedComPtr<IPortableDevice> device;
HRESULT hr = device.CreateInstance(__uuidof(PortableDevice), NULL,
CLSCTX_INPROC_SERVER);
if (FAILED(hr))
return base::win::ScopedComPtr<IPortableDevice>();
hr = device->Open(pnp_device_id.c_str(), client_info.get());
if (SUCCEEDED(hr))
return device;
if (hr == E_ACCESSDENIED)
DPLOG(ERROR) << "Access denied to open the device";
return base::win::ScopedComPtr<IPortableDevice>();
}
base::PlatformFileError GetFileEntryInfo(
IPortableDevice* device,
const base::string16& object_id,
base::PlatformFileInfo* file_entry_info) {
DCHECK(device);
DCHECK(!object_id.empty());
DCHECK(file_entry_info);
MTPDeviceObjectEntry entry;
if (!GetMTPDeviceObjectEntry(device, object_id, &entry))
return base::PLATFORM_FILE_ERROR_NOT_FOUND;
file_entry_info->size = entry.size;
file_entry_info->is_directory = entry.is_directory;
file_entry_info->is_symbolic_link = false;
file_entry_info->last_modified = entry.last_modified_time;
file_entry_info->last_accessed = entry.last_modified_time;
file_entry_info->creation_time = base::Time();
return base::PLATFORM_FILE_OK;
}
bool GetDirectoryEntries(IPortableDevice* device,
const base::string16& directory_object_id,
MTPDeviceObjectEntries* object_entries) {
return GetMTPDeviceObjectEntries(device, directory_object_id,
base::string16(), object_entries);
}
HRESULT GetFileStreamForObject(IPortableDevice* device,
const base::string16& file_object_id,
IStream** file_stream,
DWORD* optimal_transfer_size) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(device);
DCHECK(!file_object_id.empty());
base::win::ScopedComPtr<IPortableDeviceContent> content =
GetDeviceContent(device);
if (!content)
return E_FAIL;
base::win::ScopedComPtr<IPortableDeviceResources> resources;
HRESULT hr = content->Transfer(resources.Receive());
if (FAILED(hr))
return hr;
return resources->GetStream(file_object_id.c_str(), WPD_RESOURCE_DEFAULT,
STGM_READ, optimal_transfer_size,
file_stream);
}
DWORD CopyDataChunkToLocalFile(IStream* stream,
const base::FilePath& local_path,
size_t optimal_transfer_size) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(stream);
DCHECK(!local_path.empty());
if (optimal_transfer_size == 0U)
return 0U;
DWORD bytes_read = 0;
std::string buffer;
HRESULT hr = stream->Read(WriteInto(&buffer, optimal_transfer_size + 1),
optimal_transfer_size, &bytes_read);
// IStream::Read() returns S_FALSE when the actual number of bytes read from
// the stream object is less than the number of bytes requested (aka
// |optimal_transfer_size|). This indicates the end of the stream has been
// reached.
if (FAILED(hr))
return 0U;
DCHECK_GT(bytes_read, 0U);
CHECK_LE(bytes_read, buffer.length());
int data_len =
base::checked_numeric_cast<int>(
std::min(bytes_read,
base::checked_numeric_cast<DWORD>(buffer.length())));
if (file_util::AppendToFile(local_path, buffer.c_str(), data_len) != data_len)
return 0U;
return data_len;
}
base::string16 GetObjectIdFromName(IPortableDevice* device,
const base::string16& parent_id,
const base::string16& object_name) {
MTPDeviceObjectEntries object_entries;
if (!GetMTPDeviceObjectEntries(device, parent_id, object_name,
&object_entries) ||
object_entries.empty())
return base::string16();
// TODO(thestig): This DCHECK can fail. Multiple MTP objects can have
// the same name. Handle the situation gracefully. Refer to crbug.com/169930
// for more details.
DCHECK_EQ(1U, object_entries.size());
return object_entries[0].object_id;
}
} // namespace media_transfer_protocol