// 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_source.h" #include #include "base/bind.h" #include "base/callback_helpers.h" #include "base/files/file_util.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> ReadDataOnWorkerThread(base::ScopedFD fd) { constexpr size_t kChunkSize = 1024; std::vector 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& data, const std::string& charset_input) { std::u16string output; base::StringPiece piece(reinterpret_cast(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(piece.data()[0]) == kByteOrderMark[0] && static_cast(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(piece.data()[0]) == kByteOrderMark[1] && static_cast(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& dnd_actions) { dnd_actions_ = dnd_actions; } void DataSource::Target(const absl::optional& 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>& 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::max(); int html_rank = std::numeric_limits::max(); int image_rank = std::numeric_limits::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& 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& 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