blob: 4c9c5418800fa7d3eb42b4ca103e8240579006c6 [file] [log] [blame]
// Copyright 2017 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 "components/exo/data_offer.h"
#include <fcntl.h>
#include <stdio.h>
#include <memory>
#include <string>
#include <vector>
#include "base/containers/flat_set.h"
#include "base/files/file_util.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "cc/test/pixel_comparator.h"
#include "cc/test/pixel_test_utils.h"
#include "components/exo/data_device.h"
#include "components/exo/data_exchange_delegate.h"
#include "components/exo/data_offer_delegate.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/exo_test_data_exchange_delegate.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"
namespace exo {
namespace {
using DataOfferTest = test::ExoTestBase;
class TestDataOfferDelegate : public DataOfferDelegate {
public:
TestDataOfferDelegate() {}
// Called at the top of the data device's destructor, to give observers a
// chance to remove themselves.
void OnDataOfferDestroying(DataOffer* offer) override {}
// Called when |mime_type| is offered by the client.
void OnOffer(const std::string& mime_type) override {
mime_types_.insert(mime_type);
}
// Called when possible |source_actions| is offered by the client.
void OnSourceActions(
const base::flat_set<DndAction>& source_actions) override {
source_actions_ = source_actions;
}
// Called when current |action| is offered by the client.
void OnAction(DndAction dnd_action) override { dnd_action_ = dnd_action; }
const base::flat_set<std::string>& mime_types() const { return mime_types_; }
const base::flat_set<DndAction>& source_actions() const {
return source_actions_;
}
DndAction dnd_action() const { return dnd_action_; }
private:
base::flat_set<std::string> mime_types_;
base::flat_set<DndAction> source_actions_;
DndAction dnd_action_ = DndAction::kNone;
DISALLOW_COPY_AND_ASSIGN(TestDataOfferDelegate);
};
bool ReadString(base::ScopedFD fd, std::string* out) {
std::array<char, 128> buffer;
char* it = buffer.begin();
while (it != buffer.end()) {
int result = read(fd.get(), it, buffer.end() - it);
PCHECK(-1 != result);
if (result == 0)
break;
it += result;
}
*out = std::string(reinterpret_cast<char*>(buffer.data()),
(it - buffer.begin()) / sizeof(char));
return true;
}
bool ReadString16(base::ScopedFD fd, base::string16* out) {
std::array<char, 128> buffer;
char* it = buffer.begin();
while (it != buffer.end()) {
int result = read(fd.get(), it, buffer.end() - it);
PCHECK(-1 != result);
if (result == 0)
break;
it += result;
}
*out = base::string16(reinterpret_cast<base::char16*>(buffer.data()),
(it - buffer.begin()) / sizeof(base::char16));
return true;
}
TEST_F(DataOfferTest, SetTextDropData) {
base::flat_set<DndAction> source_actions;
source_actions.insert(DndAction::kCopy);
source_actions.insert(DndAction::kMove);
ui::OSExchangeData data;
data.SetString(base::string16(base::ASCIIToUTF16("Test data")));
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
EXPECT_EQ(0u, delegate.mime_types().size());
EXPECT_EQ(0u, delegate.source_actions().size());
EXPECT_EQ(DndAction::kNone, delegate.dnd_action());
TestDataExchangeDelegate data_exchange_delegate;
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
data_offer.SetSourceActions(source_actions);
data_offer.SetActions(base::flat_set<DndAction>(), DndAction::kMove);
EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-8"));
EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-16"));
EXPECT_EQ(2u, delegate.source_actions().size());
EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kCopy));
EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kMove));
EXPECT_EQ(DndAction::kMove, delegate.dnd_action());
}
TEST_F(DataOfferTest, SetHTMLDropData) {
const std::string html_data = "Test HTML data 🔥 ❄";
base::flat_set<DndAction> source_actions;
source_actions.insert(DndAction::kCopy);
source_actions.insert(DndAction::kMove);
ui::OSExchangeData data;
data.SetHtml(base::UTF8ToUTF16(html_data), GURL());
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
EXPECT_EQ(0u, delegate.mime_types().size());
EXPECT_EQ(0u, delegate.source_actions().size());
EXPECT_EQ(DndAction::kNone, delegate.dnd_action());
TestDataExchangeDelegate data_exchange_delegate;
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
data_offer.SetSourceActions(source_actions);
data_offer.SetActions(base::flat_set<DndAction>(), DndAction::kMove);
EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-8"));
EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-16"));
EXPECT_EQ(2u, delegate.source_actions().size());
EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kCopy));
EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kMove));
EXPECT_EQ(DndAction::kMove, delegate.dnd_action());
base::ScopedFD read, write;
std::string result;
EXPECT_TRUE(base::CreatePipe(&read, &write));
data_offer.Receive("text/html;charset=utf-8", std::move(write));
ReadString(std::move(read), &result);
EXPECT_EQ(result, html_data);
base::string16 result16;
EXPECT_TRUE(base::CreatePipe(&read, &write));
data_offer.Receive("text/html;charset=utf-16", std::move(write));
ReadString16(std::move(read), &result16);
EXPECT_EQ(result16, base::UTF8ToUTF16(html_data));
}
TEST_F(DataOfferTest, SetFileDropData) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
data.SetFilename(base::FilePath("/test/downloads/file"));
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
EXPECT_EQ(1u, delegate.mime_types().size());
EXPECT_EQ(1u, delegate.mime_types().count("text/uri-list"));
}
TEST_F(DataOfferTest, SetPickleDropData) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
base::Pickle pickle;
pickle.WriteUInt32(1); // num files
pickle.WriteString("filesystem:chrome-extension://path/to/file1");
pickle.WriteInt64(1000); // file size
pickle.WriteString("id"); // filesystem id
data.SetPickledData(
ui::ClipboardFormatType::GetType("chromium/x-file-system-files"), pickle);
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
EXPECT_EQ(1u, delegate.mime_types().size());
EXPECT_EQ(1u, delegate.mime_types().count("text/uri-list"));
}
TEST_F(DataOfferTest, ReceiveString) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
data.SetString(base::ASCIIToUTF16("Test data"));
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/plain", std::move(write_pipe));
std::string result;
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ("Test data", result);
base::ScopedFD read_pipe_16;
base::ScopedFD write_pipe_16;
ASSERT_TRUE(base::CreatePipe(&read_pipe_16, &write_pipe_16));
data_offer.Receive("text/plain;charset=utf-16", std::move(write_pipe_16));
base::string16 result_16;
ASSERT_TRUE(ReadString16(std::move(read_pipe_16), &result_16));
EXPECT_EQ(base::ASCIIToUTF16("Test data"), result_16);
base::ScopedFD read_pipe_8;
base::ScopedFD write_pipe_8;
ASSERT_TRUE(base::CreatePipe(&read_pipe_8, &write_pipe_8));
data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe_8));
std::string result_8;
ASSERT_TRUE(ReadString(std::move(read_pipe_8), &result_8));
EXPECT_EQ("Test data", result_8);
}
TEST_F(DataOfferTest, ReceiveHTML) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
data.SetHtml(base::ASCIIToUTF16("Test HTML data"), GURL());
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
base::ScopedFD read_pipe_16;
base::ScopedFD write_pipe_16;
ASSERT_TRUE(base::CreatePipe(&read_pipe_16, &write_pipe_16));
data_offer.Receive("text/html;charset=utf-16", std::move(write_pipe_16));
base::string16 result_16;
ASSERT_TRUE(ReadString16(std::move(read_pipe_16), &result_16));
EXPECT_EQ(base::ASCIIToUTF16("Test HTML data"), result_16);
base::ScopedFD read_pipe_8;
base::ScopedFD write_pipe_8;
ASSERT_TRUE(base::CreatePipe(&read_pipe_8, &write_pipe_8));
data_offer.Receive("text/html;charset=utf-8", std::move(write_pipe_8));
std::string result_8;
ASSERT_TRUE(ReadString(std::move(read_pipe_8), &result_8));
EXPECT_EQ("Test HTML data", result_8);
}
TEST_F(DataOfferTest, ReceiveUriList) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
data.SetFilename(base::FilePath("/test/downloads/file"));
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/uri-list", std::move(write_pipe));
std::string result;
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ("file:///test/downloads/file", result);
}
TEST_F(DataOfferTest, ReceiveUriListFromPickle_ReceiveBeforeUrlIsResolved) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
base::Pickle pickle;
pickle.WriteUInt32(1); // num files
pickle.WriteString("filesystem:chrome-extension://path/to/file1");
pickle.WriteInt64(1000); // file size
pickle.WriteString("id"); // filesystem id
data.SetPickledData(
ui::ClipboardFormatType::GetType("chromium/x-file-system-files"), pickle);
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
base::ScopedFD read_pipe1;
base::ScopedFD write_pipe1;
ASSERT_TRUE(base::CreatePipe(&read_pipe1, &write_pipe1));
base::ScopedFD read_pipe2;
base::ScopedFD write_pipe2;
ASSERT_TRUE(base::CreatePipe(&read_pipe2, &write_pipe2));
// Receive is called (twice) before UrlsFromPickleCallback runs.
data_offer.Receive("text/uri-list", std::move(write_pipe1));
data_offer.Receive("text/uri-list", std::move(write_pipe2));
// Run callback with a resolved URL.
std::vector<GURL> urls;
urls.push_back(
GURL("content://org.chromium.arc.chromecontentprovider/path/to/file1"));
data_exchange_delegate.RunSendPickleCallback(urls);
std::string result1;
ASSERT_TRUE(ReadString(std::move(read_pipe1), &result1));
EXPECT_EQ("content://org.chromium.arc.chromecontentprovider/path/to/file1",
result1);
std::string result2;
ASSERT_TRUE(ReadString(std::move(read_pipe2), &result2));
EXPECT_EQ("content://org.chromium.arc.chromecontentprovider/path/to/file1",
result2);
}
TEST_F(DataOfferTest,
ReceiveUriListFromPickle_ReceiveBeforeEmptyUrlIsReturned) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
ui::OSExchangeData data;
base::Pickle pickle;
pickle.WriteUInt32(1); // num files
pickle.WriteString("filesystem:chrome-extension://path/to/file1");
pickle.WriteInt64(1000); // file size
pickle.WriteString("id"); // filesystem id
data.SetPickledData(
ui::ClipboardFormatType::GetType("chromium/x-file-system-files"), pickle);
data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
// Receive is called before UrlsCallback runs.
data_offer.Receive("text/uri-list", std::move(write_pipe));
// Run callback with an empty URL.
std::vector<GURL> urls;
urls.push_back(GURL(""));
data_exchange_delegate.RunSendPickleCallback(urls);
base::string16 result;
ASSERT_TRUE(ReadString16(std::move(read_pipe), &result));
EXPECT_EQ(base::ASCIIToUTF16(""), result);
}
TEST_F(DataOfferTest, SetClipboardDataPlainText) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
{
ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
writer.WriteText(base::UTF8ToUTF16("Test data"));
}
auto* window = CreateTestWindowInShellWithBounds(gfx::Rect());
data_offer.SetClipboardData(
*ui::Clipboard::GetForCurrentThread(),
data_exchange_delegate.GetDataTransferEndpointType(window));
EXPECT_EQ(3u, delegate.mime_types().size());
EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-8"));
EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-16"));
EXPECT_EQ(1u, delegate.mime_types().count("UTF8_STRING"));
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
// Read as utf-8.
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe));
std::string result;
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ("Test data", result);
// Read a second time.
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe));
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ("Test data", result);
// Read as utf-16.
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/plain;charset=utf-16", std::move(write_pipe));
base::string16 result16;
ASSERT_TRUE(ReadString16(std::move(read_pipe), &result16));
EXPECT_EQ("Test data", base::UTF16ToUTF8(result16));
}
TEST_F(DataOfferTest, SetClipboardDataHTML) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
{
ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
writer.WriteHTML(base::UTF8ToUTF16("Test data"), "");
}
auto* window = CreateTestWindowInShellWithBounds(gfx::Rect());
data_offer.SetClipboardData(
*ui::Clipboard::GetForCurrentThread(),
data_exchange_delegate.GetDataTransferEndpointType(window));
EXPECT_EQ(2u, delegate.mime_types().size());
EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-8"));
EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-16"));
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/html;charset=utf-8", std::move(write_pipe));
std::string result;
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ("Test data", result);
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/html;charset=utf-16", std::move(write_pipe));
base::string16 result16;
ASSERT_TRUE(ReadString16(std::move(read_pipe), &result16));
EXPECT_EQ("Test data", base::UTF16ToUTF8(result16));
}
TEST_F(DataOfferTest, SetClipboardDataRTF) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
TestDataExchangeDelegate data_exchange_delegate;
{
ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
writer.WriteRTF("Test data");
}
auto* window = CreateTestWindowInShellWithBounds(gfx::Rect());
data_offer.SetClipboardData(
*ui::Clipboard::GetForCurrentThread(),
data_exchange_delegate.GetDataTransferEndpointType(window));
EXPECT_EQ(1u, delegate.mime_types().size());
EXPECT_EQ(1u, delegate.mime_types().count("text/rtf"));
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("text/rtf", std::move(write_pipe));
std::string result;
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ("Test data", result);
}
TEST_F(DataOfferTest, SetClipboardDataImage) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
SkBitmap image;
image.allocN32Pixels(10, 10);
image.eraseColor(SK_ColorMAGENTA);
TestDataExchangeDelegate data_exchange_delegate;
{
ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
writer.WriteImage(image);
}
auto* window = CreateTestWindowInShellWithBounds(gfx::Rect());
data_offer.SetClipboardData(
*ui::Clipboard::GetForCurrentThread(),
data_exchange_delegate.GetDataTransferEndpointType(window));
EXPECT_EQ(1u, delegate.mime_types().size());
EXPECT_EQ(1u, delegate.mime_types().count("image/png"));
base::ScopedFD read_pipe;
base::ScopedFD write_pipe;
base::ScopedFD read_pipe2;
base::ScopedFD write_pipe2;
std::string result;
// Call Receive() twice in quick succession. Requires RunUntilIdle() since
// processing is done on worker thread.
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
ASSERT_TRUE(base::CreatePipe(&read_pipe2, &write_pipe2));
data_offer.Receive("image/png", std::move(write_pipe));
data_offer.Receive("image/png", std::move(write_pipe2));
task_environment()->RunUntilIdle();
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
SkBitmap decoded;
ASSERT_TRUE(gfx::PNGCodec::Decode(
reinterpret_cast<const unsigned char*>(result.data()), result.size(),
&decoded));
EXPECT_TRUE(cc::MatchesBitmap(
image, decoded, cc::ExactPixelComparator(/*discard_alpha=*/false)));
std::string good = result;
ASSERT_TRUE(ReadString(std::move(read_pipe2), &result));
EXPECT_EQ(good, result);
// Receive() should now return immediately with result from cache.
ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
data_offer.Receive("image/png", std::move(write_pipe));
ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
EXPECT_EQ(good, result);
}
TEST_F(DataOfferTest, AcceptWithNull) {
TestDataOfferDelegate delegate;
DataOffer data_offer(&delegate);
data_offer.Accept(nullptr);
}
} // namespace
} // namespace exo