| // Copyright (c) 2012 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. |
| |
| #import <Foundation/Foundation.h> |
| #import <ImageCaptureCore/ImageCaptureCore.h> |
| |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/scoped_nsobject.h" |
| #include "base/mac/sdk_forward_declarations.h" |
| #include "base/macros.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "chrome/browser/media_galleries/mac/mtp_device_delegate_impl_mac.h" |
| #include "components/storage_monitor/image_capture_device_manager.h" |
| #include "components/storage_monitor/test_storage_monitor.h" |
| #include "content/public/test/test_browser_thread_bundle.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| const char kDeviceId[] = "id"; |
| const char kDevicePath[] = "/ic:id"; |
| const char kTestFileContents[] = "test"; |
| |
| } // namespace |
| |
| @interface MockMTPICCameraDevice : ICCameraDevice { |
| @private |
| base::scoped_nsobject<NSMutableArray> allMediaFiles_; |
| } |
| |
| - (void)addMediaFile:(ICCameraFile*)file; |
| |
| @end |
| |
| @implementation MockMTPICCameraDevice |
| |
| - (NSString*)mountPoint { |
| return @"mountPoint"; |
| } |
| |
| - (NSString*)name { |
| return @"name"; |
| } |
| |
| - (NSString*)UUIDString { |
| return base::SysUTF8ToNSString(kDeviceId); |
| } |
| |
| - (ICDeviceType)type { |
| return ICDeviceTypeCamera; |
| } |
| |
| - (void)requestOpenSession { |
| } |
| |
| - (void)requestCloseSession { |
| } |
| |
| - (NSArray*)mediaFiles { |
| return allMediaFiles_; |
| } |
| |
| - (void)addMediaFile:(ICCameraFile*)file { |
| if (!allMediaFiles_.get()) |
| allMediaFiles_.reset([[NSMutableArray alloc] init]); |
| [allMediaFiles_ addObject:file]; |
| } |
| |
| - (void)requestDownloadFile:(ICCameraFile*)file |
| options:(NSDictionary*)options |
| downloadDelegate:(id<ICCameraDeviceDownloadDelegate>)downloadDelegate |
| didDownloadSelector:(SEL)selector |
| contextInfo:(void*)contextInfo { |
| base::FilePath saveDir(base::SysNSStringToUTF8( |
| [[options objectForKey:ICDownloadsDirectoryURL] path])); |
| std::string saveAsFilename = |
| base::SysNSStringToUTF8([options objectForKey:ICSaveAsFilename]); |
| // It appears that the ImageCapture library adds an extension to the requested |
| // filename. Do that here to require a rename. |
| saveAsFilename += ".jpg"; |
| base::FilePath toBeSaved = saveDir.Append(saveAsFilename); |
| ASSERT_EQ(static_cast<int>(strlen(kTestFileContents)), |
| base::WriteFile(toBeSaved, kTestFileContents, |
| strlen(kTestFileContents))); |
| |
| NSMutableDictionary* returnOptions = |
| [NSMutableDictionary dictionaryWithDictionary:options]; |
| [returnOptions setObject:base::SysUTF8ToNSString(saveAsFilename) |
| forKey:ICSavedFilename]; |
| |
| [static_cast<NSObject<ICCameraDeviceDownloadDelegate>*>(downloadDelegate) |
| didDownloadFile:file |
| error:nil |
| options:returnOptions |
| contextInfo:contextInfo]; |
| } |
| |
| @end |
| |
| @interface MockMTPICCameraFile : ICCameraFile { |
| @private |
| base::scoped_nsobject<NSString> name_; |
| base::scoped_nsobject<NSDate> date_; |
| } |
| |
| - (id)init:(NSString*)name; |
| |
| @end |
| |
| @implementation MockMTPICCameraFile |
| |
| - (id)init:(NSString*)name { |
| if ((self = [super init])) { |
| name_.reset([name retain]); |
| date_.reset([[NSDate dateWithNaturalLanguageString:@"12/12/12"] retain]); |
| } |
| return self; |
| } |
| |
| - (NSString*)name { |
| return name_.get(); |
| } |
| |
| - (NSString*)UTI { |
| return base::mac::CFToNSCast(kUTTypeImage); |
| } |
| |
| - (NSDate*)modificationDate { |
| return date_.get(); |
| } |
| |
| - (NSDate*)creationDate { |
| return date_.get(); |
| } |
| |
| - (off_t)fileSize { |
| return 1000; |
| } |
| |
| @end |
| |
| class MTPDeviceDelegateImplMacTest : public testing::Test { |
| public: |
| MTPDeviceDelegateImplMacTest() : camera_(NULL), delegate_(NULL) {} |
| |
| void SetUp() override { |
| storage_monitor::TestStorageMonitor* monitor = |
| storage_monitor::TestStorageMonitor::CreateAndInstall(); |
| manager_.SetNotifications(monitor->receiver()); |
| |
| camera_ = [MockMTPICCameraDevice alloc]; |
| id<ICDeviceBrowserDelegate> delegate = manager_.device_browser_delegate(); |
| [delegate deviceBrowser:manager_.device_browser_for_test() |
| didAddDevice:camera_ |
| moreComing:NO]; |
| |
| delegate_ = new MTPDeviceDelegateImplMac(kDeviceId, kDevicePath); |
| } |
| |
| void TearDown() override { |
| id<ICDeviceBrowserDelegate> delegate = manager_.device_browser_delegate(); |
| [delegate deviceBrowser:manager_.device_browser_for_test() |
| didRemoveDevice:camera_ |
| moreGoing:NO]; |
| |
| delegate_->CancelPendingTasksAndDeleteDelegate(); |
| |
| storage_monitor::TestStorageMonitor::Destroy(); |
| } |
| |
| void OnError(base::WaitableEvent* event, base::File::Error error) { |
| error_ = error; |
| event->Signal(); |
| } |
| |
| void OverlappedOnError(base::WaitableEvent* event, |
| base::File::Error error) { |
| overlapped_error_ = error; |
| event->Signal(); |
| } |
| |
| void OnFileInfo(base::WaitableEvent* event, |
| const base::File::Info& info) { |
| error_ = base::File::FILE_OK; |
| info_ = info; |
| event->Signal(); |
| } |
| |
| void OnReadDir(base::WaitableEvent* event, |
| storage::AsyncFileUtil::EntryList files, |
| bool has_more) { |
| error_ = base::File::FILE_OK; |
| ASSERT_FALSE(has_more); |
| file_list_ = std::move(files); |
| event->Signal(); |
| } |
| |
| void OverlappedOnReadDir(base::WaitableEvent* event, |
| storage::AsyncFileUtil::EntryList files, |
| bool has_more) { |
| overlapped_error_ = base::File::FILE_OK; |
| ASSERT_FALSE(has_more); |
| overlapped_file_list_ = std::move(files); |
| event->Signal(); |
| } |
| |
| void OnDownload(base::WaitableEvent* event, |
| const base::File::Info& file_info, |
| const base::FilePath& local_path) { |
| error_ = base::File::FILE_OK; |
| event->Signal(); |
| } |
| |
| base::File::Error GetFileInfo(const base::FilePath& path, |
| base::File::Info* info) { |
| base::WaitableEvent wait(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| delegate_->GetFileInfo( |
| path, |
| base::Bind(&MTPDeviceDelegateImplMacTest::OnFileInfo, |
| base::Unretained(this), |
| &wait), |
| base::Bind(&MTPDeviceDelegateImplMacTest::OnError, |
| base::Unretained(this), |
| &wait)); |
| test_browser_thread_bundle_.RunUntilIdle(); |
| EXPECT_TRUE(wait.IsSignaled()); |
| *info = info_; |
| return error_; |
| } |
| |
| base::File::Error ReadDir(const base::FilePath& path) { |
| base::WaitableEvent wait(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| delegate_->ReadDirectory( |
| path, |
| base::BindRepeating(&MTPDeviceDelegateImplMacTest::OnReadDir, |
| base::Unretained(this), &wait), |
| base::Bind(&MTPDeviceDelegateImplMacTest::OnError, |
| base::Unretained(this), &wait)); |
| test_browser_thread_bundle_.RunUntilIdle(); |
| wait.Wait(); |
| return error_; |
| } |
| |
| base::File::Error DownloadFile( |
| const base::FilePath& path, |
| const base::FilePath& local_path) { |
| base::WaitableEvent wait(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| delegate_->CreateSnapshotFile( |
| path, local_path, |
| base::Bind(&MTPDeviceDelegateImplMacTest::OnDownload, |
| base::Unretained(this), |
| &wait), |
| base::Bind(&MTPDeviceDelegateImplMacTest::OnError, |
| base::Unretained(this), |
| &wait)); |
| test_browser_thread_bundle_.RunUntilIdle(); |
| wait.Wait(); |
| return error_; |
| } |
| |
| protected: |
| content::TestBrowserThreadBundle test_browser_thread_bundle_; |
| |
| base::ScopedTempDir temp_dir_; |
| storage_monitor::ImageCaptureDeviceManager manager_; |
| MockMTPICCameraDevice* camera_; |
| |
| // This object needs special deletion inside the above |task_runner_|. |
| MTPDeviceDelegateImplMac* delegate_; |
| |
| base::File::Error error_; |
| base::File::Info info_; |
| storage::AsyncFileUtil::EntryList file_list_; |
| |
| base::File::Error overlapped_error_; |
| storage::AsyncFileUtil::EntryList overlapped_file_list_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MTPDeviceDelegateImplMacTest); |
| }; |
| |
| TEST_F(MTPDeviceDelegateImplMacTest, TestGetRootFileInfo) { |
| base::File::Info info; |
| // Making a fresh delegate should have a single file entry for the synthetic |
| // root directory, with the name equal to the device id string. |
| EXPECT_EQ(base::File::FILE_OK, |
| GetFileInfo(base::FilePath(kDevicePath), &info)); |
| EXPECT_TRUE(info.is_directory); |
| EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, |
| GetFileInfo(base::FilePath("/nonexistent"), &info)); |
| |
| // Signal the delegate that no files are coming. |
| delegate_->NoMoreItems(); |
| |
| EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); |
| EXPECT_EQ(0U, file_list_.size()); |
| } |
| |
| TEST_F(MTPDeviceDelegateImplMacTest, TestOverlappedReadDir) { |
| base::Time time1 = base::Time::Now(); |
| base::File::Info info1; |
| info1.size = 1; |
| info1.is_directory = false; |
| info1.is_symbolic_link = false; |
| info1.last_modified = time1; |
| info1.last_accessed = time1; |
| info1.creation_time = time1; |
| delegate_->ItemAdded("name1", info1); |
| |
| base::WaitableEvent wait(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| |
| delegate_->ReadDirectory( |
| base::FilePath(kDevicePath), |
| base::BindRepeating(&MTPDeviceDelegateImplMacTest::OnReadDir, |
| base::Unretained(this), &wait), |
| base::Bind(&MTPDeviceDelegateImplMacTest::OnError, base::Unretained(this), |
| &wait)); |
| |
| delegate_->ReadDirectory( |
| base::FilePath(kDevicePath), |
| base::BindRepeating(&MTPDeviceDelegateImplMacTest::OverlappedOnReadDir, |
| base::Unretained(this), &wait), |
| base::Bind(&MTPDeviceDelegateImplMacTest::OverlappedOnError, |
| base::Unretained(this), &wait)); |
| |
| // Signal the delegate that no files are coming. |
| delegate_->NoMoreItems(); |
| |
| test_browser_thread_bundle_.RunUntilIdle(); |
| wait.Wait(); |
| |
| EXPECT_EQ(base::File::FILE_OK, error_); |
| EXPECT_EQ(1U, file_list_.size()); |
| EXPECT_EQ(base::File::FILE_OK, overlapped_error_); |
| EXPECT_EQ(1U, overlapped_file_list_.size()); |
| } |
| |
| TEST_F(MTPDeviceDelegateImplMacTest, TestGetFileInfo) { |
| base::Time time1 = base::Time::Now(); |
| base::File::Info info1; |
| info1.size = 1; |
| info1.is_directory = false; |
| info1.is_symbolic_link = false; |
| info1.last_modified = time1; |
| info1.last_accessed = time1; |
| info1.creation_time = time1; |
| delegate_->ItemAdded("name1", info1); |
| |
| base::File::Info info; |
| EXPECT_EQ(base::File::FILE_OK, |
| GetFileInfo(base::FilePath("/ic:id/name1"), &info)); |
| EXPECT_EQ(info1.size, info.size); |
| EXPECT_EQ(info1.is_directory, info.is_directory); |
| EXPECT_EQ(info1.last_modified, info.last_modified); |
| EXPECT_EQ(info1.last_accessed, info.last_accessed); |
| EXPECT_EQ(info1.creation_time, info.creation_time); |
| |
| info1.size = 2; |
| delegate_->ItemAdded("name2", info1); |
| delegate_->NoMoreItems(); |
| |
| EXPECT_EQ(base::File::FILE_OK, |
| GetFileInfo(base::FilePath("/ic:id/name2"), &info)); |
| EXPECT_EQ(info1.size, info.size); |
| |
| EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); |
| |
| ASSERT_EQ(2U, file_list_.size()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[0].type); |
| EXPECT_EQ("name1", file_list_[0].name.value()); |
| |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[1].type); |
| EXPECT_EQ("name2", file_list_[1].name.value()); |
| } |
| |
| TEST_F(MTPDeviceDelegateImplMacTest, TestDirectoriesAndSorting) { |
| base::Time time1 = base::Time::Now(); |
| base::File::Info info1; |
| info1.size = 1; |
| info1.is_directory = false; |
| info1.is_symbolic_link = false; |
| info1.last_modified = time1; |
| info1.last_accessed = time1; |
| info1.creation_time = time1; |
| delegate_->ItemAdded("name2", info1); |
| |
| info1.is_directory = true; |
| delegate_->ItemAdded("dir2", info1); |
| delegate_->ItemAdded("dir1", info1); |
| |
| info1.is_directory = false; |
| delegate_->ItemAdded("name1", info1); |
| delegate_->NoMoreItems(); |
| |
| EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); |
| |
| ASSERT_EQ(4U, file_list_.size()); |
| EXPECT_EQ("dir1", file_list_[0].name.value()); |
| EXPECT_EQ("dir2", file_list_[1].name.value()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[2].type); |
| EXPECT_EQ("name1", file_list_[2].name.value()); |
| |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[3].type); |
| EXPECT_EQ("name2", file_list_[3].name.value()); |
| } |
| |
| TEST_F(MTPDeviceDelegateImplMacTest, SubDirectories) { |
| base::Time time1 = base::Time::Now(); |
| base::File::Info info1; |
| info1.size = 0; |
| info1.is_directory = true; |
| info1.is_symbolic_link = false; |
| info1.last_modified = time1; |
| info1.last_accessed = time1; |
| info1.creation_time = time1; |
| delegate_->ItemAdded("dir1", info1); |
| |
| info1.size = 1; |
| info1.is_directory = false; |
| info1.is_symbolic_link = false; |
| info1.last_modified = time1; |
| info1.last_accessed = time1; |
| info1.creation_time = time1; |
| delegate_->ItemAdded("dir1/name1", info1); |
| |
| info1.is_directory = true; |
| info1.size = 0; |
| delegate_->ItemAdded("dir2", info1); |
| |
| info1.is_directory = false; |
| info1.size = 1; |
| delegate_->ItemAdded("dir2/name2", info1); |
| |
| info1.is_directory = true; |
| info1.size = 0; |
| delegate_->ItemAdded("dir2/subdir", info1); |
| |
| info1.is_directory = false; |
| info1.size = 1; |
| delegate_->ItemAdded("dir2/subdir/name3", info1); |
| delegate_->ItemAdded("name4", info1); |
| |
| delegate_->NoMoreItems(); |
| |
| EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); |
| ASSERT_EQ(3U, file_list_.size()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::DIRECTORY, file_list_[0].type); |
| EXPECT_EQ("dir1", file_list_[0].name.value()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::DIRECTORY, file_list_[1].type); |
| EXPECT_EQ("dir2", file_list_[1].name.value()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[2].type); |
| EXPECT_EQ("name4", file_list_[2].name.value()); |
| |
| EXPECT_EQ(base::File::FILE_OK, |
| ReadDir(base::FilePath(kDevicePath).Append("dir1"))); |
| ASSERT_EQ(1U, file_list_.size()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[0].type); |
| EXPECT_EQ("name1", file_list_[0].name.value()); |
| |
| EXPECT_EQ(base::File::FILE_OK, |
| ReadDir(base::FilePath(kDevicePath).Append("dir2"))); |
| ASSERT_EQ(2U, file_list_.size()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[0].type); |
| EXPECT_EQ("name2", file_list_[0].name.value()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::DIRECTORY, file_list_[1].type); |
| EXPECT_EQ("subdir", file_list_[1].name.value()); |
| |
| EXPECT_EQ(base::File::FILE_OK, |
| ReadDir(base::FilePath(kDevicePath) |
| .Append("dir2").Append("subdir"))); |
| ASSERT_EQ(1U, file_list_.size()); |
| EXPECT_EQ(filesystem::mojom::FsFileType::REGULAR_FILE, file_list_[0].type); |
| EXPECT_EQ("name3", file_list_[0].name.value()); |
| |
| EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, |
| ReadDir(base::FilePath(kDevicePath) |
| .Append("dir2").Append("subdir").Append("subdir"))); |
| EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, |
| ReadDir(base::FilePath(kDevicePath) |
| .Append("dir3").Append("subdir"))); |
| EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, |
| ReadDir(base::FilePath(kDevicePath).Append("dir3"))); |
| } |
| |
| TEST_F(MTPDeviceDelegateImplMacTest, TestDownload) { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| base::Time t1 = base::Time::Now(); |
| base::File::Info info; |
| info.size = 4; |
| info.is_directory = false; |
| info.is_symbolic_link = false; |
| info.last_modified = t1; |
| info.last_accessed = t1; |
| info.creation_time = t1; |
| std::string kTestFileName("filename"); |
| base::scoped_nsobject<MockMTPICCameraFile> picture1( |
| [[MockMTPICCameraFile alloc] |
| init:base::SysUTF8ToNSString(kTestFileName)]); |
| [camera_ addMediaFile:picture1]; |
| delegate_->ItemAdded(kTestFileName, info); |
| delegate_->NoMoreItems(); |
| |
| EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); |
| ASSERT_EQ(1U, file_list_.size()); |
| ASSERT_EQ("filename", file_list_[0].name.value()); |
| |
| EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, |
| DownloadFile(base::FilePath("/ic:id/nonexist"), |
| temp_dir_.GetPath().Append("target"))); |
| |
| EXPECT_EQ(base::File::FILE_OK, |
| DownloadFile(base::FilePath("/ic:id/filename"), |
| temp_dir_.GetPath().Append("target"))); |
| std::string contents; |
| EXPECT_TRUE( |
| base::ReadFileToString(temp_dir_.GetPath().Append("target"), &contents)); |
| EXPECT_EQ(kTestFileContents, contents); |
| } |