|  | // Copyright 2020 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <string_view> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/safe_base_name.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/strings/strcat.h" | 
|  | #include "base/strings/string_number_conversions.h" | 
|  | #include "base/test/bind.h" | 
|  | #include "base/uuid.h" | 
|  | #include "build/build_config.h" | 
|  | #include "chrome/browser/webshare/share_service_impl.h" | 
|  | #include "chrome/test/base/chrome_render_view_host_test_harness.h" | 
|  | #include "content/public/browser/browser_context.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "storage/browser/blob/blob_data_builder.h" | 
|  | #include "storage/browser/blob/blob_impl.h" | 
|  | #include "storage/browser/blob/blob_storage_context.h" | 
|  | #include "third_party/blink/public/mojom/blob/serialized_blob.mojom.h" | 
|  | #include "url/gurl.h" | 
|  |  | 
|  | using blink::mojom::ShareError; | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | #include "chrome/browser/sharesheet/sharesheet_types.h" | 
|  | #include "chrome/browser/webshare/chromeos/sharesheet_client.h" | 
|  | #include "chromeos/components/sharesheet/constants.h" | 
|  | #endif | 
|  | #if BUILDFLAG(IS_MAC) | 
|  | #include "chrome/browser/webshare/mac/sharing_service_operation.h" | 
|  | #include "third_party/blink/public/mojom/webshare/webshare.mojom.h" | 
|  | #endif | 
|  | #if BUILDFLAG(IS_WIN) | 
|  | #include "chrome/browser/webshare/win/scoped_share_operation_fake_components.h" | 
|  | #endif | 
|  |  | 
|  | class ShareServiceUnitTest : public ChromeRenderViewHostTestHarness { | 
|  | public: | 
|  | ~ShareServiceUnitTest() override = default; | 
|  |  | 
|  | void SetUp() override { | 
|  | ChromeRenderViewHostTestHarness::SetUp(); | 
|  | ShareServiceImpl::Create( | 
|  | main_rfh(), share_service_remote_.BindNewPipeAndPassReceiver()); | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | webshare::SharesheetClient::SetSharesheetCallbackForTesting( | 
|  | base::BindRepeating(&ShareServiceUnitTest::AcceptShareRequest)); | 
|  | #endif | 
|  | #if BUILDFLAG(IS_MAC) | 
|  | webshare::SharingServiceOperation::SetSharePickerCallbackForTesting( | 
|  | base::BindRepeating(&ShareServiceUnitTest::AcceptShareRequest)); | 
|  | #endif | 
|  | #if BUILDFLAG(IS_WIN) | 
|  | ASSERT_NO_FATAL_FAILURE(scoped_fake_components_.SetUp()); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | ShareError ShareGeneratedFileData(std::string_view extension, | 
|  | const std::string& content_type, | 
|  | unsigned file_length = 100, | 
|  | unsigned file_count = 1) { | 
|  | const std::string kTitle; | 
|  | const std::string kText; | 
|  | const GURL kUrl; | 
|  | std::vector<blink::mojom::SharedFilePtr> files; | 
|  | files.reserve(file_count); | 
|  | for (unsigned index = 0; index < file_count; ++index) { | 
|  | files.push_back(CreateSharedFile( | 
|  | base::FilePath::FromASCII( | 
|  | base::StrCat({"share", base::NumberToString(index), extension})), | 
|  | content_type, file_length)); | 
|  | } | 
|  |  | 
|  | ShareError result; | 
|  | base::RunLoop run_loop; | 
|  | share_service_remote_->Share( | 
|  | kTitle, kText, kUrl, std::move(files), | 
|  | base::BindLambdaForTesting([&result, &run_loop](ShareError error) { | 
|  | result = error; | 
|  | run_loop.Quit(); | 
|  | })); | 
|  | run_loop.Run(); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | bool IsDangerousFilename(base::FilePath::StringViewType path) { | 
|  | return ShareServiceImpl::IsDangerousFilename(base::FilePath(path)); | 
|  | } | 
|  |  | 
|  | private: | 
|  | blink::mojom::SharedFilePtr CreateSharedFile(const base::FilePath& name, | 
|  | const std::string& content_type, | 
|  | unsigned file_length) { | 
|  | const std::string uuid = base::Uuid::GenerateRandomV4().AsLowercaseString(); | 
|  |  | 
|  | auto blob = blink::mojom::SerializedBlob::New(); | 
|  | blob->uuid = uuid; | 
|  | blob->content_type = content_type; | 
|  | blob->size = file_length; | 
|  |  | 
|  | base::RunLoop run_loop; | 
|  | auto blob_context_getter = browser_context()->GetBlobStorageContext(); | 
|  | content::GetIOThreadTaskRunner({})->PostTaskAndReply( | 
|  | FROM_HERE, | 
|  | base::BindLambdaForTesting( | 
|  | [&blob_context_getter, &blob, &uuid, &content_type, file_length]() { | 
|  | storage::BlobImpl::Create( | 
|  | blob_context_getter.Run()->AddFinishedBlob( | 
|  | CreateBuilder(uuid, content_type, file_length)), | 
|  | blob->blob.InitWithNewPipeAndPassReceiver()); | 
|  | }), | 
|  | base::BindLambdaForTesting([&run_loop]() { run_loop.Quit(); })); | 
|  |  | 
|  | run_loop.Run(); | 
|  | return blink::mojom::SharedFile::New(*base::SafeBaseName::Create(name), | 
|  | std::move(blob)); | 
|  | } | 
|  |  | 
|  | static std::unique_ptr<storage::BlobDataBuilder> CreateBuilder( | 
|  | const std::string& uuid, | 
|  | const std::string& content_type, | 
|  | unsigned file_length) { | 
|  | auto builder = std::make_unique<storage::BlobDataBuilder>(uuid); | 
|  | builder->set_content_type(content_type); | 
|  | const std::string contents(file_length, '*'); | 
|  | builder->AppendData(contents); | 
|  | return builder; | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | static void AcceptShareRequest( | 
|  | content::WebContents* web_contents, | 
|  | const std::vector<base::FilePath>& file_paths, | 
|  | const std::vector<std::string>& content_types, | 
|  | const std::vector<uint64_t>& file_sizes, | 
|  | const std::string& text, | 
|  | const std::string& title, | 
|  | sharesheet::DeliveredCallback delivered_callback) { | 
|  | std::move(delivered_callback).Run(sharesheet::SharesheetResult::kSuccess); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(IS_MAC) | 
|  | static void AcceptShareRequest( | 
|  | content::WebContents* web_contents, | 
|  | const std::vector<base::FilePath>& file_paths, | 
|  | const std::string& text, | 
|  | const std::string& title, | 
|  | const GURL& url, | 
|  | blink::mojom::ShareService::ShareCallback close_callback) { | 
|  | std::move(close_callback).Run(blink::mojom::ShareError::OK); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(IS_WIN) | 
|  | webshare::ScopedShareOperationFakeComponents scoped_fake_components_; | 
|  | #endif | 
|  | mojo::Remote<blink::mojom::ShareService> share_service_remote_; | 
|  | }; | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, FileCount) { | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".txt", "text/plain", 1234, | 
|  | kMaxSharedFileCount)); | 
|  | EXPECT_EQ(ShareError::PERMISSION_DENIED, | 
|  | ShareGeneratedFileData(".txt", "text/plain", 1234, | 
|  | kMaxSharedFileCount + 1)); | 
|  | } | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, DangerousFilename) { | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL(""))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("."))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("./"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL(".\\"))); | 
|  |  | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("a.a"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("zzz.zzz"))); | 
|  |  | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("a/a"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("zzz/zzz"))); | 
|  |  | 
|  | EXPECT_FALSE(IsDangerousFilename(FILE_PATH_LITERAL("1.XBM"))); | 
|  | EXPECT_FALSE(IsDangerousFilename(FILE_PATH_LITERAL("2.bMP"))); | 
|  | EXPECT_FALSE(IsDangerousFilename(FILE_PATH_LITERAL("3.Flac"))); | 
|  | EXPECT_FALSE(IsDangerousFilename(FILE_PATH_LITERAL("4.webM"))); | 
|  | } | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, DangerousMimeType) { | 
|  | EXPECT_TRUE(ShareServiceImpl::IsDangerousMimeType("")); | 
|  | EXPECT_TRUE(ShareServiceImpl::IsDangerousMimeType("/")); | 
|  |  | 
|  | EXPECT_TRUE(ShareServiceImpl::IsDangerousMimeType("a/a")); | 
|  | EXPECT_TRUE(ShareServiceImpl::IsDangerousMimeType("zzz/zzz")); | 
|  |  | 
|  | EXPECT_TRUE(ShareServiceImpl::IsDangerousMimeType("audio/Flac")); | 
|  | EXPECT_TRUE(ShareServiceImpl::IsDangerousMimeType("Video/webm")); | 
|  |  | 
|  | EXPECT_FALSE(ShareServiceImpl::IsDangerousMimeType("audio/mp3")); | 
|  | EXPECT_FALSE(ShareServiceImpl::IsDangerousMimeType("audio/mpeg")); | 
|  | } | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, Multimedia) { | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".avif", "image/avif")); | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".bmp", "image/bmp")); | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".xbm", "image/x-xbitmap")); | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".flac", "audio/flac")); | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".webm", "video/webm")); | 
|  | } | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, PortableDocumentFormat) { | 
|  | EXPECT_EQ(ShareError::OK, ShareGeneratedFileData(".pdf", "application/pdf")); | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_WIN) | 
|  | TEST_F(ShareServiceUnitTest, ReservedNames) { | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("CON"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("PRN"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("AUX"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("NUL"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("COM1"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("COM9"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("LPT1"))); | 
|  | EXPECT_TRUE(IsDangerousFilename(FILE_PATH_LITERAL("LPT9"))); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | // On Chrome OS, like Android, we prevent sharing of Android applications. | 
|  | TEST_F(ShareServiceUnitTest, AndroidPackage) { | 
|  | EXPECT_EQ(ShareError::PERMISSION_DENIED, | 
|  | ShareGeneratedFileData(".apk", "text/plain")); | 
|  | EXPECT_EQ(ShareError::PERMISSION_DENIED, | 
|  | ShareGeneratedFileData(".dex", "text/plain")); | 
|  | EXPECT_EQ(ShareError::PERMISSION_DENIED, | 
|  | ShareGeneratedFileData(".txt", "vnd.android.package-archive")); | 
|  | } | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, TotalBytes) { | 
|  | EXPECT_EQ(ShareError::OK, | 
|  | ShareGeneratedFileData(".txt", "text/plain", | 
|  | kMaxSharedFileBytes / kMaxSharedFileCount, | 
|  | kMaxSharedFileCount)); | 
|  | EXPECT_EQ( | 
|  | ShareError::PERMISSION_DENIED, | 
|  | ShareGeneratedFileData(".txt", "text/plain", | 
|  | (kMaxSharedFileBytes / kMaxSharedFileCount) + 1, | 
|  | kMaxSharedFileCount)); | 
|  | } | 
|  |  | 
|  | TEST_F(ShareServiceUnitTest, FileBytes) { | 
|  | EXPECT_EQ(ShareError::OK, | 
|  | ShareGeneratedFileData(".txt", "text/plain", kMaxSharedFileBytes)); | 
|  | EXPECT_EQ( | 
|  | ShareError::PERMISSION_DENIED, | 
|  | ShareGeneratedFileData(".txt", "text/plain", kMaxSharedFileBytes + 1)); | 
|  | } | 
|  | #endif |