| // Copyright 2014 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/ash/file_manager/fileapi_util.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/bind.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/ash/file_system_provider/fake_extension_provider.h" |
| #include "chrome/browser/ash/file_system_provider/service.h" |
| #include "chrome/browser/ash/file_system_provider/service_factory.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/test_utils.h" |
| #include "storage/browser/file_system/external_mount_points.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "storage/browser/test/async_file_test_helper.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" |
| #include "ui/shell_dialogs/selected_file_info.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace file_manager { |
| namespace util { |
| namespace { |
| |
| // Helper class that sets up a temporary file system. |
| class TempFileSystem { |
| public: |
| TempFileSystem(Profile* profile, const GURL& appURL) |
| : name_(base::UnguessableToken::Create().ToString()), |
| appURL_(appURL), |
| origin_(url::Origin::Create(appURL)), |
| file_system_context_( |
| GetFileSystemContextForSourceURL(profile, appURL)) {} |
| |
| ~TempFileSystem() { |
| storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(name_); |
| } |
| |
| // Finishes setting up the temporary file system. Must be called before use. |
| bool SetUp() { |
| if (!temp_dir_.CreateUniqueTempDir()) { |
| return false; |
| } |
| if (!storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem( |
| name_, storage::kFileSystemTypeLocal, |
| storage::FileSystemMountOption(), temp_dir_.GetPath())) { |
| return false; |
| } |
| |
| // Grant the test extension the ability to access the just created |
| // file system. |
| file_system_context_->external_backend()->GrantFileAccessToOrigin( |
| origin_, base::FilePath(name_)); |
| return true; |
| } |
| |
| bool TearDown() { |
| return storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem( |
| name_); |
| } |
| |
| // For the given FileSystemURL creates a file. |
| base::File::Error CreateFile(const storage::FileSystemURL& url) { |
| return storage::AsyncFileTestHelper::CreateFile(file_system_context_, url); |
| } |
| |
| // Creates an external file system URL for the given path. |
| storage::FileSystemURL CreateFileSystemURL(const std::string& path) { |
| return file_system_context_->CreateCrackedFileSystemURL( |
| blink::StorageKey(origin_), storage::kFileSystemTypeExternal, |
| base::FilePath().Append(name_).Append( |
| base::FilePath::FromUTF8Unsafe(path))); |
| } |
| |
| private: |
| const std::string name_; |
| const GURL appURL_; |
| const url::Origin origin_; |
| storage::FileSystemContext* const file_system_context_; |
| base::ScopedTempDir temp_dir_; |
| }; |
| |
| // Parameter for the unit test to allow us to test both System Files App case |
| // and Extension Files app. |
| enum FilesAppMode { |
| EXTENSION_FILES_APP, |
| SYSTEM_FILES_APP, |
| }; |
| |
| class FileManagerFileAPIUtilTest |
| : public ::testing::TestWithParam<FilesAppMode> { |
| public: |
| FileManagerFileAPIUtilTest() { |
| if (GetParam() == SYSTEM_FILES_APP) { |
| feature_list_.InitAndEnableFeature(chromeos::features::kFilesSWA); |
| } |
| } |
| |
| // Carries information on how to create a FileSystemURL for a given file name. |
| // For !valid orders we create a test URL. Otherwise, we use temp file system. |
| struct FileSystemURLOrder { |
| std::string file_name; |
| bool valid; |
| }; |
| |
| void SetUp() override { |
| testing::Test::SetUp(); |
| profile_manager_ = std::make_unique<TestingProfileManager>( |
| TestingBrowserProcess::GetGlobal()); |
| ASSERT_TRUE(profile_manager_->SetUp()); |
| profile_ = profile_manager_->CreateTestingProfile("testing_profile"); |
| } |
| |
| void TearDown() override { |
| profile_manager_->DeleteAllTestingProfiles(); |
| profile_ = nullptr; |
| profile_manager_.reset(); |
| } |
| |
| TestingProfile* GetProfile() { return profile_; } |
| |
| protected: |
| // Checks if the conversion of FileDefinition to EntryDefinition works |
| // correctly for the given |appURLStr| and a set of |orders|. If the |
| // order indicates that the file should not be created, we expect the |
| // conversion to return base::File::FILE_ERROR_NOT_FOUND error. Otherwise, |
| // we expect base::File::FILE_OK status. |
| void CheckConvertFileDefinitionListToEntryDefinitionList( |
| const std::string& appURLStr, |
| const std::vector<FileSystemURLOrder>& orders) { |
| GURL appURL(appURLStr); |
| ASSERT_TRUE(appURL.is_valid()); |
| auto temp_file_system = std::make_unique<TempFileSystem>(profile_, appURL); |
| ASSERT_TRUE(temp_file_system->SetUp()); |
| |
| std::vector<base::File::Error> errors; |
| std::vector<FileDefinition> file_definitions; |
| for (const FileSystemURLOrder& order : orders) { |
| storage::FileSystemURL fs_url; |
| if (order.valid) { |
| fs_url = temp_file_system->CreateFileSystemURL(order.file_name); |
| errors.push_back(base::File::FILE_OK); |
| } else { |
| fs_url = storage::FileSystemURL::CreateForTest( |
| blink::StorageKey(url::Origin::Create(appURL)), |
| storage::kFileSystemTypeExternal, base::FilePath(order.file_name)); |
| errors.push_back(base::File::FILE_ERROR_NOT_FOUND); |
| } |
| file_definitions.push_back({.virtual_path = fs_url.virtual_path()}); |
| } |
| |
| base::RunLoop run_loop; |
| EntryDefinitionListCallback callback = base::BindOnce( |
| [](std::unique_ptr<TempFileSystem> temp_file_system, |
| std::vector<base::File::Error> errors, |
| base::OnceClosure quit_closure, |
| std::unique_ptr<EntryDefinitionList> entries) { |
| ASSERT_EQ(errors.size(), entries->size()); |
| for (size_t i = 0; i < errors.size(); ++i) { |
| const EntryDefinition& entry_def = (*entries)[i]; |
| EXPECT_EQ(errors[i], entry_def.error) |
| << "for " << entry_def.full_path << " at " << i; |
| } |
| |
| EXPECT_TRUE(temp_file_system->TearDown()); |
| std::move(quit_closure).Run(); |
| }, |
| std::move(temp_file_system), std::move(errors), run_loop.QuitClosure()); |
| ConvertFileDefinitionListToEntryDefinitionList( |
| GetFileSystemContextForSourceURL(profile_, appURL), |
| url::Origin::Create(appURL), file_definitions, std::move(callback)); |
| run_loop.Run(); |
| } |
| |
| const std::string file_system_id_ = "test-filesystem"; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| content::BrowserTaskEnvironment task_environment_; |
| std::unique_ptr<TestingProfileManager> profile_manager_; |
| TestingProfile* profile_; |
| }; |
| |
| // Passes the |result| to the |output| pointer. |
| void PassFileChooserFileInfoList(FileChooserFileInfoList* output, |
| FileChooserFileInfoList result) { |
| for (const auto& file : result) |
| output->push_back(file->Clone()); |
| } |
| |
| TEST_P(FileManagerFileAPIUtilTest, |
| ConvertSelectedFileInfoListToFileChooserFileInfoList) { |
| Profile* const profile = GetProfile(); |
| const std::string extension_id = "abc"; |
| auto fake_provider = |
| ash::file_system_provider::FakeExtensionProvider::Create(extension_id); |
| const auto kProviderId = fake_provider->GetId(); |
| auto* service = ash::file_system_provider::Service::Get(profile); |
| service->RegisterProvider(std::move(fake_provider)); |
| service->MountFileSystem( |
| kProviderId, ash::file_system_provider::MountOptions(file_system_id_, |
| "Test FileSystem")); |
| |
| // Obtain the file system context. |
| content::StoragePartition* const partition = |
| profile->GetStoragePartitionForUrl(GURL("http://example.com")); |
| ASSERT_TRUE(partition); |
| storage::FileSystemContext* const context = partition->GetFileSystemContext(); |
| ASSERT_TRUE(context); |
| |
| // Prepare the test input. |
| SelectedFileInfoList selected_info_list; |
| |
| // Native file. |
| { |
| ui::SelectedFileInfo info; |
| info.file_path = base::FilePath(FILE_PATH_LITERAL("/native/File 1.txt")); |
| info.local_path = base::FilePath(FILE_PATH_LITERAL("/native/File 1.txt")); |
| info.display_name = "display_name"; |
| selected_info_list.push_back(info); |
| } |
| |
| const std::string path = FILE_PATH_LITERAL(base::StrCat( |
| {"/provided/", extension_id, ":", file_system_id_, ":/hello.txt"})); |
| // Non-native file with cache. |
| { |
| ui::SelectedFileInfo info; |
| info.file_path = base::FilePath(path); |
| info.local_path = base::FilePath(FILE_PATH_LITERAL("/native/cache/xxx")); |
| info.display_name = "display_name"; |
| selected_info_list.push_back(info); |
| } |
| |
| // Non-native file without. |
| { |
| ui::SelectedFileInfo info; |
| info.file_path = base::FilePath(path); |
| selected_info_list.push_back(info); |
| } |
| |
| // Run the test target. |
| FileChooserFileInfoList result; |
| ConvertSelectedFileInfoListToFileChooserFileInfoList( |
| context, GURL("http://example.com"), selected_info_list, |
| base::BindOnce(&PassFileChooserFileInfoList, &result)); |
| content::RunAllTasksUntilIdle(); |
| |
| // Check the result. |
| ASSERT_EQ(3u, result.size()); |
| |
| EXPECT_TRUE(result[0]->is_native_file()); |
| EXPECT_EQ(FILE_PATH_LITERAL("/native/File 1.txt"), |
| result[0]->get_native_file()->file_path.value()); |
| EXPECT_EQ(u"display_name", result[0]->get_native_file()->display_name); |
| |
| EXPECT_TRUE(result[1]->is_native_file()); |
| EXPECT_EQ(FILE_PATH_LITERAL("/native/cache/xxx"), |
| result[1]->get_native_file()->file_path.value()); |
| EXPECT_EQ(u"display_name", result[1]->get_native_file()->display_name); |
| |
| EXPECT_TRUE(result[2]->is_file_system()); |
| EXPECT_TRUE(result[2]->get_file_system()->url.is_valid()); |
| const storage::FileSystemURL url = |
| context->CrackURLInFirstPartyContext(result[2]->get_file_system()->url); |
| EXPECT_EQ(GURL("http://example.com"), url.origin().GetURL()); |
| EXPECT_EQ(storage::kFileSystemTypeIsolated, url.mount_type()); |
| EXPECT_EQ(storage::kFileSystemTypeProvided, url.type()); |
| EXPECT_EQ(55u, result[2]->get_file_system()->length); |
| } |
| |
| TEST_P(FileManagerFileAPIUtilTest, |
| ConvertFileDefinitionListToEntryDefinitionListExtension) { |
| std::vector<FileSystemURLOrder> orders = { |
| {.file_name = "x.txt", .valid = true}, |
| {.file_name = "no-such-file.txt", .valid = false}, |
| {.file_name = "z.txt", .valid = true}, |
| }; |
| CheckConvertFileDefinitionListToEntryDefinitionList("chrome-extension://abc", |
| orders); |
| CheckConvertFileDefinitionListToEntryDefinitionList("chrome-extension://abc/", |
| orders); |
| CheckConvertFileDefinitionListToEntryDefinitionList( |
| "chrome-extension://abc/efg", orders); |
| } |
| |
| TEST_P(FileManagerFileAPIUtilTest, |
| ConvertFileDefinitionListToEntryDefinitionListApp) { |
| std::vector<FileSystemURLOrder> orders = { |
| {.file_name = "a.txt", .valid = false}, |
| {.file_name = "b.txt", .valid = false}, |
| {.file_name = "i-am-a-file.txt", .valid = true}, |
| }; |
| CheckConvertFileDefinitionListToEntryDefinitionList("chrome://file-manager", |
| orders); |
| CheckConvertFileDefinitionListToEntryDefinitionList("chrome://file-manager/", |
| orders); |
| CheckConvertFileDefinitionListToEntryDefinitionList( |
| "chrome://file-manager/abc", orders); |
| } |
| |
| TEST_P(FileManagerFileAPIUtilTest, |
| ConvertFileDefinitionListToEntryDefinitionNullContext) { |
| Profile* const profile = GetProfile(); |
| const GURL appURL("chrome-extension://abc/"); |
| auto temp_file_system = std::make_unique<TempFileSystem>(profile, appURL); |
| ASSERT_TRUE(temp_file_system->SetUp()); |
| storage::FileSystemURL x_file_url = |
| temp_file_system->CreateFileSystemURL("."); |
| FileDefinition x_fd = {.virtual_path = x_file_url.virtual_path()}; |
| |
| // Check a simple case where the context is already null before we have |
| // a chance to call the conversion function. |
| base::RunLoop run_loop; |
| EntryDefinitionListCallback callback = base::BindOnce( |
| [](std::unique_ptr<TempFileSystem> temp_file_system, |
| base::OnceClosure quit_closure, |
| std::unique_ptr<EntryDefinitionList> entries) { |
| ASSERT_EQ(1, entries->size()); |
| EXPECT_EQ(base::File::FILE_ERROR_INVALID_OPERATION, |
| entries->at(0).error); |
| EXPECT_TRUE(temp_file_system->TearDown()); |
| std::move(quit_closure).Run(); |
| }, |
| std::move(temp_file_system), run_loop.QuitClosure()); |
| |
| ConvertFileDefinitionListToEntryDefinitionList( |
| nullptr, url::Origin::Create(appURL), {x_fd}, std::move(callback)); |
| run_loop.Run(); |
| } |
| |
| TEST_P(FileManagerFileAPIUtilTest, |
| ConvertFileDefinitionListToEntryDefinitionContextReset) { |
| Profile* const profile = GetProfile(); |
| const GURL appURL("chrome-extension://abc/"); |
| auto temp_file_system = std::make_unique<TempFileSystem>(profile, appURL); |
| ASSERT_TRUE(temp_file_system->SetUp()); |
| storage::FileSystemURL x_file_url = |
| temp_file_system->CreateFileSystemURL("."); |
| FileDefinition x_fd = {.virtual_path = x_file_url.virtual_path()}; |
| scoped_refptr<storage::FileSystemContext> file_system_context = |
| GetFileSystemContextForSourceURL(profile, appURL); |
| |
| base::RunLoop run_loop; |
| EntryDefinitionListCallback callback = base::BindOnce( |
| [](std::unique_ptr<TempFileSystem> temp_file_system, |
| base::OnceClosure quit_closure, |
| std::unique_ptr<EntryDefinitionList> entries) { |
| ASSERT_EQ(1, entries->size()); |
| EXPECT_EQ(base::File::FILE_OK, entries->at(0).error); |
| EXPECT_TRUE(temp_file_system->TearDown()); |
| std::move(quit_closure).Run(); |
| }, |
| std::move(temp_file_system), run_loop.QuitClosure()); |
| |
| // Check the case where the context is not null, but is reset to null as |
| // soon as function call is completed. Conversion takes place on a |
| // different thread, after the function call returns. However, since |
| // it holds to a copy of a scoped pointer we expect it to succeed. |
| ConvertFileDefinitionListToEntryDefinitionList(file_system_context, |
| url::Origin::Create(appURL), |
| {x_fd}, std::move(callback)); |
| file_system_context.reset(); |
| |
| run_loop.Run(); |
| } |
| |
| TEST_P(FileManagerFileAPIUtilTest, IsFileManagerURL) { |
| EXPECT_TRUE(IsFileManagerURL(GetFileManagerURL())); |
| EXPECT_TRUE(IsFileManagerURL(GetFileManagerURL().Resolve("/some/path"))); |
| EXPECT_TRUE(IsFileManagerURL( |
| GetFileManagerURL().Resolve("/some/path").Resolve("#anchor"))); |
| EXPECT_TRUE(IsFileManagerURL(GetFileManagerURL() |
| .Resolve("/some/path") |
| .Resolve("#anchor") |
| .Resolve("?a=b"))); |
| EXPECT_FALSE(IsFileManagerURL(GURL("chrome://not-file-manager"))); |
| EXPECT_FALSE(IsFileManagerURL(GURL("chrome://not-file-manager/"))); |
| EXPECT_FALSE(IsFileManagerURL( |
| GURL("chrome-extension://iamnotafilemanagerextensionid"))); |
| EXPECT_FALSE(IsFileManagerURL( |
| GURL("chrome-extension://iamnotafilemanagerextensionid/"))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(FilesAppMode, |
| FileManagerFileAPIUtilTest, |
| ::testing::Values(EXTENSION_FILES_APP, |
| SYSTEM_FILES_APP)); |
| |
| } // namespace |
| } // namespace util |
| } // namespace file_manager |