// 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 "base/message_loop/message_loop.h" #include "ppapi/c/pp_errors.h" #include "ppapi/c/ppb_file_io.h" #include "ppapi/c/ppb_file_ref.h" #include "ppapi/c/ppb_file_system.h" #include "ppapi/proxy/file_system_resource.h" #include "ppapi/proxy/locking_resource_releaser.h" #include "ppapi/proxy/plugin_message_filter.h" #include "ppapi/proxy/ppapi_message_utils.h" #include "ppapi/proxy/ppapi_messages.h" #include "ppapi/proxy/ppapi_proxy_test.h" #include "ppapi/shared_impl/proxy_lock.h" #include "ppapi/shared_impl/scoped_pp_var.h" #include "ppapi/shared_impl/var.h" #include "ppapi/thunk/enter.h" #include "ppapi/thunk/ppb_file_system_api.h" #include "ppapi/thunk/thunk.h" using ppapi::proxy::ResourceMessageTestSink; using ppapi::thunk::EnterResource; using ppapi::thunk::PPB_FileSystem_API; namespace ppapi { namespace proxy { namespace { const int64_t kExpectedFileSystemSize = 100; const int64_t kQuotaRequestAmount1 = 10; const int64_t kQuotaRequestAmount2 = 20; class MockCompletionCallback { public: MockCompletionCallback() : called_(false) {} bool called() { return called_; } int32_t result() { return result_; } static void Callback(void* user_data, int32_t result) { MockCompletionCallback* that = reinterpret_cast<MockCompletionCallback*>(user_data); that->called_ = true; that->result_ = result; } private: bool called_; int32_t result_; }; class MockRequestQuotaCallback { public: MockRequestQuotaCallback() : called_(false) {} bool called() { return called_; } int64_t result() { return result_; } void Reset() { called_ = false; } void Callback(int64_t result) { ASSERT_FALSE(called_); called_ = true; result_ = result; } private: bool called_; int64_t result_; }; class FileSystemResourceTest : public PluginProxyTest { public: const PPB_FileSystem_1_0* file_system_iface; const PPB_FileRef_1_1* file_ref_iface; const PPB_FileIO_1_1* file_io_iface; FileSystemResourceTest() : file_system_iface(thunk::GetPPB_FileSystem_1_0_Thunk()), file_ref_iface(thunk::GetPPB_FileRef_1_1_Thunk()), file_io_iface(thunk::GetPPB_FileIO_1_1_Thunk()) { } void SendReply(const ResourceMessageCallParams& params, int32_t result, const IPC::Message& nested_message) { ResourceMessageReplyParams reply_params(params.pp_resource(), params.sequence()); reply_params.set_result(result); PluginMessageFilter::DispatchResourceReplyForTest( reply_params, nested_message); } void SendOpenReply(const ResourceMessageCallParams& params, int32_t result) { SendReply(params, result, PpapiPluginMsg_FileSystem_OpenReply()); } // Opens the given file system. void OpenFileSystem(PP_Resource file_system) { MockCompletionCallback cb; int32_t result = file_system_iface->Open( file_system, kExpectedFileSystemSize, PP_MakeCompletionCallback(&MockCompletionCallback::Callback, &cb)); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); // Should have sent two new "open" messages to the browser and renderer. ResourceMessageTestSink::ResourceCallVector open_messages = sink().GetAllResourceCallsMatching(PpapiHostMsg_FileSystem_Open::ID); ASSERT_EQ(2U, open_messages.size()); sink().ClearMessages(); // The resource is expecting two replies. SendOpenReply(open_messages[0].first, PP_OK); SendOpenReply(open_messages[1].first, PP_OK); ASSERT_TRUE(cb.called()); ASSERT_EQ(PP_OK, cb.result()); } // Opens the given file in the given file system. Since there is no host, // the file handle will be invalid. void OpenFile(PP_Resource file_io, PP_Resource file_ref, PP_Resource file_system) { MockCompletionCallback cb; int32_t result = file_io_iface->Open( file_io, file_ref, PP_FILEOPENFLAG_WRITE, PP_MakeCompletionCallback(&MockCompletionCallback::Callback, &cb)); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); // Should have sent an "open" message. ResourceMessageCallParams params; IPC::Message msg; ASSERT_TRUE(sink().GetFirstResourceCallMatching( PpapiHostMsg_FileIO_Open::ID, ¶ms, &msg)); sink().ClearMessages(); // Send a success reply. ResourceMessageReplyParams reply_params(params.pp_resource(), params.sequence()); reply_params.set_result(PP_OK); PluginMessageFilter::DispatchResourceReplyForTest( reply_params, PpapiPluginMsg_FileIO_OpenReply(file_system, 0 /* max_written_offset */)); } }; } // namespace // Test that Open fails if either host returns failure. The other tests exercise // the case where both hosts return PP_OK. TEST_F(FileSystemResourceTest, OpenFailure) { // Fail if the first reply doesn't return PP_OK. { LockingResourceReleaser file_system( file_system_iface->Create(pp_instance(), PP_FILESYSTEMTYPE_LOCALTEMPORARY)); MockCompletionCallback cb; int32_t result = file_system_iface->Open( file_system.get(), kExpectedFileSystemSize, PP_MakeCompletionCallback(&MockCompletionCallback::Callback, &cb)); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); ResourceMessageTestSink::ResourceCallVector open_messages = sink().GetAllResourceCallsMatching(PpapiHostMsg_FileSystem_Open::ID); ASSERT_EQ(2U, open_messages.size()); sink().ClearMessages(); SendOpenReply(open_messages[0].first, PP_ERROR_FAILED); SendOpenReply(open_messages[1].first, PP_OK); ASSERT_TRUE(cb.called()); ASSERT_EQ(PP_ERROR_FAILED, cb.result()); } // Fail if the second reply doesn't return PP_OK. { LockingResourceReleaser file_system( file_system_iface->Create(pp_instance(), PP_FILESYSTEMTYPE_LOCALTEMPORARY)); MockCompletionCallback cb; int32_t result = file_system_iface->Open( file_system.get(), kExpectedFileSystemSize, PP_MakeCompletionCallback(&MockCompletionCallback::Callback, &cb)); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); ResourceMessageTestSink::ResourceCallVector open_messages = sink().GetAllResourceCallsMatching(PpapiHostMsg_FileSystem_Open::ID); ASSERT_EQ(2U, open_messages.size()); sink().ClearMessages(); SendOpenReply(open_messages[0].first, PP_OK); SendOpenReply(open_messages[1].first, PP_ERROR_FAILED); ASSERT_TRUE(cb.called()); ASSERT_EQ(PP_ERROR_FAILED, cb.result()); } } TEST_F(FileSystemResourceTest, RequestQuota) { LockingResourceReleaser file_system( file_system_iface->Create(pp_instance(), PP_FILESYSTEMTYPE_LOCALTEMPORARY)); OpenFileSystem(file_system.get()); // Create and open two files in the file system. FileIOResource calls // FileSystemResource::OpenQuotaFile on success. LockingResourceReleaser file_ref1( file_ref_iface->Create(file_system.get(), "/file1")); LockingResourceReleaser file_io1(file_io_iface->Create(pp_instance())); OpenFile(file_io1.get(), file_ref1.get(), file_system.get()); LockingResourceReleaser file_ref2( file_ref_iface->Create(file_system.get(), "/file2")); LockingResourceReleaser file_io2(file_io_iface->Create(pp_instance())); OpenFile(file_io2.get(), file_ref2.get(), file_system.get()); EnterResource<PPB_FileSystem_API> enter(file_system.get(), true); ASSERT_FALSE(enter.failed()); PPB_FileSystem_API* file_system_api = enter.object(); MockRequestQuotaCallback cb1; int64_t result = file_system_api->RequestQuota( kQuotaRequestAmount1, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb1))); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); // Should have sent a "reserve quota" message, with the amount of the request // and a map of all currently open files to their max written offsets. ResourceMessageCallParams params; IPC::Message msg; ASSERT_TRUE(sink().GetFirstResourceCallMatching( PpapiHostMsg_FileSystem_ReserveQuota::ID, ¶ms, &msg)); sink().ClearMessages(); int64_t amount = 0; FileGrowthMap file_growths; ASSERT_TRUE(UnpackMessage<PpapiHostMsg_FileSystem_ReserveQuota>( msg, &amount, &file_growths)); ASSERT_EQ(kQuotaRequestAmount1, amount); ASSERT_EQ(2U, file_growths.size()); ASSERT_EQ(0, file_growths[file_io1.get()].max_written_offset); ASSERT_EQ(0, file_growths[file_io2.get()].max_written_offset); // Make another request while the "reserve quota" message is pending. MockRequestQuotaCallback cb2; result = file_system_api->RequestQuota( kQuotaRequestAmount2, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb2))); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); // No new "reserve quota" message should be sent while one is pending. ASSERT_FALSE(sink().GetFirstResourceCallMatching( PpapiHostMsg_FileSystem_ReserveQuota::ID, ¶ms, &msg)); { ProxyAutoUnlock unlock_to_prevent_deadlock; // Reply with quota reservation amount sufficient to cover both requests. // Both callbacks should be called with the requests granted. SendReply(params, PP_OK, PpapiPluginMsg_FileSystem_ReserveQuotaReply( kQuotaRequestAmount1 + kQuotaRequestAmount2, FileGrowthMapToFileSizeMapForTesting(file_growths))); } ASSERT_TRUE(cb1.called()); ASSERT_EQ(kQuotaRequestAmount1, cb1.result()); ASSERT_TRUE(cb2.called()); ASSERT_EQ(kQuotaRequestAmount2, cb2.result()); cb1.Reset(); cb2.Reset(); // All requests should fail when insufficient quota is returned to satisfy // the first request. result = file_system_api->RequestQuota( kQuotaRequestAmount1, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb1))); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); result = file_system_api->RequestQuota( kQuotaRequestAmount2, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb2))); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); ASSERT_TRUE(sink().GetFirstResourceCallMatching( PpapiHostMsg_FileSystem_ReserveQuota::ID, ¶ms, &msg)); sink().ClearMessages(); { ProxyAutoUnlock unlock_to_prevent_deadlock; // Reply with quota reservation amount insufficient to cover the first // request. SendReply(params, PP_OK, PpapiPluginMsg_FileSystem_ReserveQuotaReply( kQuotaRequestAmount1 - 1, FileGrowthMapToFileSizeMapForTesting(file_growths))); } ASSERT_TRUE(cb1.called()); ASSERT_EQ(0, cb1.result()); ASSERT_TRUE(cb2.called()); ASSERT_EQ(0, cb2.result()); cb1.Reset(); cb2.Reset(); // A new request should be made if the quota reservation is enough to satisfy // at least one request. result = file_system_api->RequestQuota( kQuotaRequestAmount1, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb1))); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); result = file_system_api->RequestQuota( kQuotaRequestAmount2, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb2))); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); ASSERT_TRUE(sink().GetFirstResourceCallMatching( PpapiHostMsg_FileSystem_ReserveQuota::ID, ¶ms, &msg)); sink().ClearMessages(); { ProxyAutoUnlock unlock_to_prevent_deadlock; // Reply with quota reservation amount sufficient only to cover the first // request. SendReply(params, PP_OK, PpapiPluginMsg_FileSystem_ReserveQuotaReply( kQuotaRequestAmount1, FileGrowthMapToFileSizeMapForTesting(file_growths))); } ASSERT_TRUE(cb1.called()); ASSERT_EQ(kQuotaRequestAmount1, cb1.result()); ASSERT_FALSE(cb2.called()); // Another request message should have been sent. ASSERT_TRUE(sink().GetFirstResourceCallMatching( PpapiHostMsg_FileSystem_ReserveQuota::ID, ¶ms, &msg)); sink().ClearMessages(); { ProxyAutoUnlock unlock_to_prevent_deadlock; // Reply with quota reservation amount sufficient to cover the second // request and some extra. SendReply(params, PP_OK, PpapiPluginMsg_FileSystem_ReserveQuotaReply( kQuotaRequestAmount1 + kQuotaRequestAmount2, FileGrowthMapToFileSizeMapForTesting(file_growths))); } ASSERT_TRUE(cb2.called()); ASSERT_EQ(kQuotaRequestAmount2, cb2.result()); cb1.Reset(); cb2.Reset(); // There is kQuotaRequestAmount1 of quota left, and a request for it should // succeed immediately. result = file_system_api->RequestQuota( kQuotaRequestAmount1, base::Bind(&MockRequestQuotaCallback::Callback, base::Unretained(&cb1))); ASSERT_EQ(kQuotaRequestAmount1, result); } } // namespace proxy } // namespace ppapi