| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/exo/data_source.h" |
| |
| #include <limits> |
| |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/i18n/character_encoding.h" |
| #include "base/i18n/icu_string_conversions.h" |
| #include "base/posix/eintr_wrapper.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/thread_pool.h" |
| #include "components/exo/data_source_delegate.h" |
| #include "components/exo/data_source_observer.h" |
| #include "components/exo/mime_utils.h" |
| #include "net/base/mime_util.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| #include "third_party/icu/source/common/unicode/ucnv.h" |
| #include "ui/base/clipboard/clipboard_constants.h" |
| |
| namespace exo { |
| |
| namespace { |
| |
| constexpr char kTextPlain[] = "text/plain"; |
| constexpr char kTextRTF[] = "text/rtf"; |
| constexpr char kTextHTML[] = "text/html"; |
| constexpr char kTextUriList[] = "text/uri-list"; |
| constexpr char kApplicationOctetStream[] = "application/octet-stream"; |
| constexpr char kWebCustomData[] = "chromium/x-web-custom-data"; |
| constexpr char kDataTransferEndpoint[] = "chromium/x-data-transfer-endpoint"; |
| |
| constexpr char kUtfPrefix[] = "UTF"; |
| constexpr char kEncoding16[] = "16"; |
| constexpr char kEncodingASCII[] = "ASCII"; |
| |
| constexpr char kUTF16Unspecified[] = "UTF-16"; |
| constexpr char kUTF16LittleEndian[] = "UTF-16LE"; |
| constexpr char kUTF16BigEndian[] = "UTF-16BE"; |
| constexpr uint8_t kByteOrderMark[] = {0xFE, 0xFF}; |
| constexpr int kByteOrderMarkSize = sizeof(kByteOrderMark); |
| |
| constexpr char kImageBitmap[] = "image/bmp"; |
| constexpr char kImagePNG[] = "image/png"; |
| constexpr char kImageAPNG[] = "image/apng"; |
| |
| absl::optional<std::vector<uint8_t>> ReadDataOnWorkerThread(base::ScopedFD fd) { |
| constexpr size_t kChunkSize = 1024; |
| std::vector<uint8_t> bytes; |
| while (true) { |
| uint8_t chunk[kChunkSize]; |
| ssize_t bytes_read = HANDLE_EINTR(read(fd.get(), chunk, kChunkSize)); |
| if (bytes_read > 0) { |
| bytes.insert(bytes.end(), chunk, chunk + bytes_read); |
| continue; |
| } |
| if (!bytes_read) |
| return bytes; |
| if (bytes_read < 0) { |
| PLOG(ERROR) << "Failed to read selection data from clipboard"; |
| return absl::nullopt; |
| } |
| } |
| } |
| |
| // Map a named character set to an integer ranking, lower is better. This is an |
| // implementation detail of DataSource::GetPreferredMimeTypes and should not be |
| // considered to have any greater meaning. In particular, these are not expected |
| // to remain stable over time. |
| int GetCharsetRank(const std::string& charset_input) { |
| std::string charset = base::ToUpperASCII(charset_input); |
| |
| // Prefer UTF-16 over all other encodings, because that's what the clipboard |
| // interface takes as input; then other unicode encodings; then any non-ASCII |
| // encoding, because most or all such encodings are super-sets of ASCII; |
| // finally, only use ASCII if nothing else is available. |
| if (base::StartsWith(charset, kUtfPrefix, base::CompareCase::SENSITIVE)) { |
| if (charset.find(kEncoding16) != std::string::npos) |
| return 0; |
| return 1; |
| } else if (charset.find(kEncodingASCII) == std::string::npos) { |
| return 2; |
| } |
| return 3; |
| } |
| |
| // Map an image MIME type to an integer ranking, lower is better. This is an |
| // implementation detail of DataSource::GetPreferredMimeTypes and should not be |
| // considered to have any greater meaning. In particular, these are not expected |
| // to remain stable over time. |
| int GetImageTypeRank(const std::string& mime_type) { |
| // Prefer PNG most of all because this format preserves the alpha channel and |
| // is lossless, followed by BMP for being lossless and fast to decode (but |
| // doesn't preserve alpha), followed by everything else. |
| if (net::MatchesMimeType(std::string(kImagePNG), mime_type) || |
| net::MatchesMimeType(std::string(kImageAPNG), mime_type)) |
| return 0; |
| if (net::MatchesMimeType(std::string(kImageBitmap), mime_type)) |
| return 1; |
| return 2; |
| } |
| |
| std::u16string CodepageToUTF16(const std::vector<uint8_t>& data, |
| const std::string& charset_input) { |
| std::u16string output; |
| base::StringPiece piece(reinterpret_cast<const char*>(data.data()), |
| data.size()); |
| const char* charset = charset_input.c_str(); |
| |
| // Despite claims in the documentation to the contrary, the ICU UTF-16 |
| // converter does not automatically detect and interpret the byte order |
| // mark. Therefore, we must do this ourselves. |
| if (!ucnv_compareNames(charset, kUTF16Unspecified) && |
| data.size() >= kByteOrderMarkSize) { |
| if (static_cast<uint8_t>(piece.data()[0]) == kByteOrderMark[0] && |
| static_cast<uint8_t>(piece.data()[1]) == kByteOrderMark[1]) { |
| // BOM is in big endian format. Consume the BOM so it doesn't get |
| // interpreted as a character. |
| piece.remove_prefix(2); |
| charset = kUTF16BigEndian; |
| } else if (static_cast<uint8_t>(piece.data()[0]) == kByteOrderMark[1] && |
| static_cast<uint8_t>(piece.data()[1]) == kByteOrderMark[0]) { |
| // BOM is in little endian format. Consume the BOM so it doesn't get |
| // interpreted as a character. |
| piece.remove_prefix(2); |
| charset = kUTF16LittleEndian; |
| } |
| } |
| |
| base::CodepageToUTF16( |
| piece, charset, base::OnStringConversionError::Type::SUBSTITUTE, &output); |
| return output; |
| } |
| |
| // Returns name parameter in application/octet-stream;name=<...>, or empty |
| // string if parsing fails. |
| std::string GetApplicationOctetStreamName(const std::string& mime_type) { |
| base::StringPairs params; |
| if (net::MatchesMimeType(std::string(kApplicationOctetStream), mime_type) && |
| net::ParseMimeType(mime_type, nullptr, ¶ms)) { |
| for (const auto& kv : params) { |
| if (kv.first == "name") |
| return kv.second; |
| } |
| } |
| return std::string(); |
| } |
| |
| } // namespace |
| |
| ScopedDataSource::ScopedDataSource(DataSource* data_source, |
| DataSourceObserver* observer) |
| : data_source_(data_source), observer_(observer) { |
| data_source_->AddObserver(observer_); |
| } |
| |
| ScopedDataSource::~ScopedDataSource() { |
| data_source_->RemoveObserver(observer_); |
| } |
| |
| DataSource::DataSource(DataSourceDelegate* delegate) |
| : delegate_(delegate), finished_(false) {} |
| |
| DataSource::~DataSource() { |
| delegate_->OnDataSourceDestroying(this); |
| for (DataSourceObserver& observer : observers_) { |
| observer.OnDataSourceDestroying(this); |
| } |
| } |
| |
| void DataSource::AddObserver(DataSourceObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void DataSource::RemoveObserver(DataSourceObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void DataSource::Offer(const std::string& mime_type) { |
| mime_types_.insert(mime_type); |
| } |
| |
| void DataSource::SetActions(const base::flat_set<DndAction>& dnd_actions) { |
| dnd_actions_ = dnd_actions; |
| } |
| |
| void DataSource::Target(const absl::optional<std::string>& mime_type) { |
| delegate_->OnTarget(mime_type); |
| } |
| |
| void DataSource::Action(DndAction action) { |
| delegate_->OnAction(action); |
| } |
| |
| void DataSource::DndDropPerformed() { |
| delegate_->OnDndDropPerformed(); |
| } |
| |
| void DataSource::Cancelled() { |
| finished_ = true; |
| read_data_weak_ptr_factory_.InvalidateWeakPtrs(); |
| delegate_->OnCancelled(); |
| } |
| |
| void DataSource::DndFinished() { |
| finished_ = true; |
| read_data_weak_ptr_factory_.InvalidateWeakPtrs(); |
| delegate_->OnDndFinished(); |
| } |
| |
| void DataSource::ReadDataForTesting(const std::string& mime_type, |
| ReadDataCallback callback) { |
| ReadData(mime_type, std::move(callback), base::DoNothing()); |
| } |
| |
| void DataSource::ReadData(const std::string& mime_type, |
| ReadDataCallback callback, |
| base::OnceClosure failure_callback) { |
| // This DataSource does not contain the requested MIME type. |
| if (mime_type.empty() || !mime_types_.count(mime_type) || finished_) { |
| std::move(failure_callback).Run(); |
| return; |
| } |
| |
| base::ScopedFD read_fd; |
| base::ScopedFD write_fd; |
| PCHECK(base::CreatePipe(&read_fd, &write_fd)); |
| delegate_->OnSend(mime_type, std::move(write_fd)); |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce(&ReadDataOnWorkerThread, std::move(read_fd)), |
| base::BindOnce( |
| &DataSource::OnDataRead, read_data_weak_ptr_factory_.GetWeakPtr(), |
| std::move(callback), mime_type, std::move(failure_callback))); |
| } |
| |
| void DataSource::OnDataRead(ReadDataCallback callback, |
| const std::string& mime_type, |
| base::OnceClosure failure_callback, |
| const absl::optional<std::vector<uint8_t>>& data) { |
| if (!data) { |
| std::move(failure_callback).Run(); |
| return; |
| } |
| std::move(callback).Run(mime_type, *data); |
| } |
| |
| void DataSource::ReadDataTransferEndpoint( |
| ReadTextDataCallback dte_reader, |
| base::RepeatingClosure failure_callback) { |
| ReadData(kDataTransferEndpoint, |
| base::BindOnce(&DataSource::OnTextRead, |
| read_data_weak_ptr_factory_.GetWeakPtr(), |
| std::move(dte_reader)), |
| failure_callback); |
| } |
| |
| void DataSource::GetDataForPreferredMimeTypes( |
| ReadTextDataCallback text_reader, |
| ReadDataCallback rtf_reader, |
| ReadTextDataCallback html_reader, |
| ReadDataCallback image_reader, |
| ReadDataCallback filenames_reader, |
| ReadFileContentsDataCallback file_contents_reader, |
| ReadDataCallback web_custom_data_reader, |
| base::RepeatingClosure failure_callback) { |
| std::string text_mime; |
| std::string rtf_mime; |
| std::string html_mime; |
| std::string image_mime; |
| std::string filenames_mime; |
| std::string file_contents_mime; |
| std::string web_custom_data_mime; |
| |
| int text_rank = std::numeric_limits<int>::max(); |
| int html_rank = std::numeric_limits<int>::max(); |
| int image_rank = std::numeric_limits<int>::max(); |
| |
| for (auto mime_type : mime_types_) { |
| if (net::MatchesMimeType(std::string(kTextPlain), mime_type) || |
| mime_type == ui::kMimeTypeLinuxUtf8String) { |
| if (text_reader.is_null()) |
| continue; |
| |
| std::string charset; |
| charset = GetCharset(mime_type); |
| int new_rank = GetCharsetRank(charset); |
| if (new_rank < text_rank) { |
| text_mime = mime_type; |
| text_rank = new_rank; |
| } |
| } else if (net::MatchesMimeType(std::string(kTextRTF), mime_type)) { |
| if (rtf_reader.is_null()) |
| continue; |
| |
| // The RTF MIME type will never have a character set because it only uses |
| // 7-bit bytes and stores character set information internally. |
| rtf_mime = mime_type; |
| } else if (net::MatchesMimeType(std::string(kTextHTML), mime_type)) { |
| if (html_reader.is_null()) |
| continue; |
| |
| auto charset = GetCharset(mime_type); |
| int new_rank = GetCharsetRank(charset); |
| if (new_rank < html_rank) { |
| html_mime = mime_type; |
| html_rank = new_rank; |
| } |
| } else if (blink::IsSupportedImageMimeType(mime_type)) { |
| if (image_reader.is_null()) |
| continue; |
| |
| int new_rank = GetImageTypeRank(mime_type); |
| if (new_rank < image_rank) { |
| image_mime = mime_type; |
| image_rank = new_rank; |
| } |
| } else if (net::MatchesMimeType(std::string(kTextUriList), mime_type)) { |
| if (filenames_reader.is_null()) |
| continue; |
| |
| filenames_mime = mime_type; |
| } else if (!GetApplicationOctetStreamName(mime_type).empty()) { |
| file_contents_mime = mime_type; |
| } else if (net::MatchesMimeType(std::string(kWebCustomData), mime_type)) { |
| web_custom_data_mime = mime_type; |
| } |
| } |
| |
| ReadData(text_mime, |
| base::BindOnce(&DataSource::OnTextRead, |
| read_data_weak_ptr_factory_.GetWeakPtr(), |
| std::move(text_reader)), |
| failure_callback); |
| ReadData(rtf_mime, std::move(rtf_reader), failure_callback); |
| ReadData(html_mime, |
| base::BindOnce(&DataSource::OnTextRead, |
| read_data_weak_ptr_factory_.GetWeakPtr(), |
| std::move(html_reader)), |
| failure_callback); |
| ReadData(image_mime, std::move(image_reader), failure_callback); |
| ReadData(filenames_mime, std::move(filenames_reader), failure_callback); |
| ReadData(file_contents_mime, |
| base::BindOnce(&DataSource::OnFileContentsRead, |
| read_data_weak_ptr_factory_.GetWeakPtr(), |
| std::move(file_contents_reader)), |
| failure_callback); |
| ReadData(web_custom_data_mime, std::move(web_custom_data_reader), |
| failure_callback); |
| } |
| |
| void DataSource::OnTextRead(ReadTextDataCallback callback, |
| const std::string& mime_type, |
| const std::vector<uint8_t>& data) { |
| std::u16string output = CodepageToUTF16(data, GetCharset(mime_type)); |
| std::move(callback).Run(mime_type, std::move(output)); |
| } |
| |
| void DataSource::OnFileContentsRead(ReadFileContentsDataCallback callback, |
| const std::string& mime_type, |
| const std::vector<uint8_t>& data) { |
| const base::FilePath filename(GetApplicationOctetStreamName(mime_type)); |
| std::move(callback).Run(mime_type, filename, data); |
| } |
| |
| bool DataSource::CanBeDataSourceForCopy(Surface* surface) const { |
| return delegate_->CanAcceptDataEventsForSurface(surface); |
| } |
| |
| } // namespace exo |