chromium/net/base/file_stream_unittest.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40284755): Remove this and spanify to fix the errors.
#pragma allow_unsafe_buffers
#endif

#include "net/base/file_stream.h"

#include <string>
#include <utility>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/current_thread.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/test_completion_callback.h"
#include "net/log/test_net_log.h"
#include "net/test/gtest_util.h"
#include "net/test/test_with_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"

IsError;
IsOk;

#if BUILDFLAG(IS_ANDROID)
#include "base/test/test_file_util.h"
#endif

namespace net {

namespace {

constexpr char kTestData[] =;
constexpr int kTestDataSize =;

// Creates an IOBufferWithSize that contains the kTestDataSize.
scoped_refptr<IOBufferWithSize> CreateTestDataBuffer() {}

}  // namespace

class FileStreamTest : public PlatformTest, public WithTaskEnvironment {};

namespace {

TEST_F(FileStreamTest, OpenExplicitClose) {}

TEST_F(FileStreamTest, OpenExplicitCloseOrphaned) {}

// Test the use of FileStream with a file handle provided at construction.
TEST_F(FileStreamTest, UseFileHandle) {}

TEST_F(FileStreamTest, UseClosedStream) {}

TEST_F(FileStreamTest, Read) {}

TEST_F(FileStreamTest, Read_EarlyDelete) {}

TEST_F(FileStreamTest, Read_FromOffset) {}

TEST_F(FileStreamTest, Write) {}

TEST_F(FileStreamTest, Write_EarlyDelete) {}

TEST_F(FileStreamTest, Write_FromOffset) {}

TEST_F(FileStreamTest, BasicReadWrite) {}

TEST_F(FileStreamTest, BasicWriteRead) {}

class TestWriteReadCompletionCallback {};

TEST_F(FileStreamTest, WriteRead) {}

class TestWriteCloseCompletionCallback {};

TEST_F(FileStreamTest, WriteClose) {}

TEST_F(FileStreamTest, OpenAndDelete) {}

// Verify that Write() errors are mapped correctly.
TEST_F(FileStreamTest, WriteError) {}

// Verify that Read() errors are mapped correctly.
TEST_F(FileStreamTest, ReadError) {}

#if BUILDFLAG(IS_WIN)
// Verifies that a FileStream will close itself if it receives a File whose
// async flag doesn't match the async state of the underlying handle.
TEST_F(FileStreamTest, AsyncFlagMismatch) {
  // Open the test file without async, then make a File with the same sync
  // handle but with the async flag set to true.
  uint32_t flags = base::File::FLAG_OPEN | base::File::FLAG_READ;
  base::File file(temp_file_path(), flags);
  base::File lying_file(file.TakePlatformFile(), true);
  ASSERT_TRUE(lying_file.IsValid());

  FileStream stream(std::move(lying_file),
                    base::SingleThreadTaskRunner::GetCurrentDefault());
  ASSERT_FALSE(stream.IsOpen());
  TestCompletionCallback callback;
  scoped_refptr<IOBufferWithSize> buf =
      base::MakeRefCounted<IOBufferWithSize>(4);
  int rv = stream.Read(buf.get(), buf->size(), callback.callback());
  EXPECT_THAT(callback.GetResult(rv), IsError(ERR_UNEXPECTED));
}
#endif

#if BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/41420277): flaky on both android and cronet bots.
TEST_F(FileStreamTest, DISABLED_ContentUriRead) {
  base::FilePath test_dir;
  base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_dir);
  test_dir = test_dir.AppendASCII("net");
  test_dir = test_dir.AppendASCII("data");
  test_dir = test_dir.AppendASCII("file_stream_unittest");
  ASSERT_TRUE(base::PathExists(test_dir));
  base::FilePath image_file = test_dir.Append(FILE_PATH_LITERAL("red.png"));

  // Insert the image into MediaStore. MediaStore will do some conversions, and
  // return the content URI.
  base::FilePath path = base::InsertImageIntoMediaStore(image_file);
  EXPECT_TRUE(path.IsContentUri());
  EXPECT_TRUE(base::PathExists(path));
  int64_t file_size;
  EXPECT_TRUE(base::GetFileSize(path, &file_size));
  EXPECT_LT(0, file_size);

  FileStream stream(base::SingleThreadTaskRunner::GetCurrentDefault());
  int flags = base::File::FLAG_OPEN | base::File::FLAG_READ |
              base::File::FLAG_ASYNC;
  TestCompletionCallback callback;
  int rv = stream.Open(path, flags, callback.callback());
  EXPECT_THAT(rv, IsError(ERR_IO_PENDING));
  EXPECT_THAT(callback.WaitForResult(), IsOk());

  int total_bytes_read = 0;

  std::string data_read;
  for (;;) {
    scoped_refptr<IOBufferWithSize> buf =
        base::MakeRefCounted<IOBufferWithSize>(4);
    rv = stream.Read(buf.get(), buf->size(), callback.callback());
    if (rv == ERR_IO_PENDING)
      rv = callback.WaitForResult();
    EXPECT_LE(0, rv);
    if (rv <= 0)
      break;
    total_bytes_read += rv;
    data_read.append(buf->data(), rv);
  }
  EXPECT_EQ(file_size, total_bytes_read);
}
#endif

#if BUILDFLAG(IS_WIN)
// A test fixture with helpers to create and connect to a named pipe for the
// sake of testing FileStream::ConnectNamedPipe().
class FileStreamPipeTest : public PlatformTest, public WithTaskEnvironment {
 protected:
  FileStreamPipeTest() = default;

  // Creates a named pipe (of name `pipe_name_`) for asynchronous use. Returns a
  // `File` wrapping it or an error.
  base::File CreatePipe() {
    base::win::ScopedHandle pipe(::CreateNamedPipeW(
        pipe_name_.c_str(),
        PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE |
            FILE_FLAG_OVERLAPPED,
        PIPE_TYPE_BYTE, /*nMaxInstances=*/1,
        /*nOutBufferSize=*/0, /*nInBufferSize=*/0, /*nDefaultTimeOut=*/0,
        /*lpSecurityAttributes=*/nullptr));
    if (pipe.IsValid()) {
      return base::File(std::move(pipe), /*async=*/true);
    }
    return base::File(base::File::GetLastFileError());
  }

  // Opens the pipe named `pipe_name_`, which must have previously been created
  // via CreatePipe(). Returns a `File` wrapping it or an error.
  base::File OpenPipe() {
    base::win::ScopedHandle pipe(
        ::CreateFileW(pipe_name_.c_str(), GENERIC_READ | GENERIC_WRITE,
                      /*dwShareMode=*/0, /*lpSecurityAttributes=*/nullptr,
                      OPEN_EXISTING, /*dwFlagsAndAttributes=*/0,
                      /*hTemplateFile=*/nullptr));
    if (!pipe.IsValid()) {
      return base::File(base::File::GetLastFileError());
    }
    return base::File(std::move(pipe));
  }

 private:
  // A random name for a pipe to be used for the test.
  const std::wstring pipe_name_{base::StrCat(
      {L"\\\\.\\pipe\\chromium.test.",
       base::ASCIIToWide(base::UnguessableToken::Create().ToString())})};
};

// Tests that FileStream::ConnectNamedPipe() works when the client has already
// opened the pipe.
TEST_F(FileStreamPipeTest, ConnectNamedPipeAfterClient) {
  base::File pipe(CreatePipe());
  ASSERT_TRUE(pipe.IsValid())
      << base::File::ErrorToString(pipe.error_details());

  FileStream pipe_stream(std::move(pipe),
                         base::SingleThreadTaskRunner::GetCurrentDefault());
  ASSERT_TRUE(pipe_stream.IsOpen());

  // Open the client end of the pipe.
  base::File client(OpenPipe());
  ASSERT_TRUE(client.IsValid())
      << base::File::ErrorToString(client.error_details());

  // Connecting should be synchronous and should not run the callback, but
  // handle both cases anyway for the sake of robustness against the unexpected.
  TestCompletionCallback callback;
  ASSERT_THAT(
      callback.GetResult(pipe_stream.ConnectNamedPipe(callback.callback())),
      IsOk());

  // Send some data over the pipe to be sure it works.
  scoped_refptr<IOBufferWithSize> write_io_buffer = CreateTestDataBuffer();
  int result = pipe_stream.Write(write_io_buffer.get(), write_io_buffer->size(),
                                 callback.callback());

  // Perform a synchronous read on the pipe.
  auto buffer = base::HeapArray<uint8_t>::WithSize(write_io_buffer->size());
  ASSERT_EQ(client.ReadAtCurrentPos(buffer.as_span()), write_io_buffer->size());

  // The write above may have returned ERR_IO_PENDING. Pump messages until it
  // completes, if so.
  ASSERT_THAT(callback.GetResult(result), write_io_buffer->size());
  ASSERT_EQ(buffer.as_span(), base::as_bytes(write_io_buffer->span()));
}

// Tests that FileStream::ConnectNamedPipe() works when called before the client
// has a chance to open the pipe.
TEST_F(FileStreamPipeTest, ConnectNamedPipeBeforeClient) {
  base::File pipe(CreatePipe());
  ASSERT_TRUE(pipe.IsValid())
      << base::File::ErrorToString(pipe.error_details());

  FileStream pipe_stream(std::move(pipe),
                         base::SingleThreadTaskRunner::GetCurrentDefault());
  ASSERT_TRUE(pipe_stream.IsOpen());

  // The client hasn't opened yet, so the connect request should wait for an
  // IO completion packet.
  TestCompletionCallback callback;
  ASSERT_THAT(pipe_stream.ConnectNamedPipe(callback.callback()),
              IsError(ERR_IO_PENDING));

  // Open the client end of the pipe.
  base::File client(OpenPipe());
  ASSERT_TRUE(client.IsValid())
      << base::File::ErrorToString(client.error_details());

  // Pump messages until the callback given to ConnectNamedPipe is run.
  ASSERT_THAT(callback.WaitForResult(), IsOk());
}

// Tests that nothing bad happens if a FileStream is destroyed after
// ConnectNamedPipe() but before a client connects.
TEST_F(FileStreamPipeTest, CloseBeforeConnect) {
  {
    base::File pipe(CreatePipe());
    ASSERT_TRUE(pipe.IsValid())
        << base::File::ErrorToString(pipe.error_details());

    FileStream pipe_stream(std::move(pipe),
                           base::SingleThreadTaskRunner::GetCurrentDefault());
    ASSERT_TRUE(pipe_stream.IsOpen());

    // The client hasn't opened yet, so the connect request should wait for an
    // IO completion packet. The callback should never be run, but it will be
    // destroyed asynchronously after the stream is closed. Give the callback a
    // `ScopedClosureRunner` that will quit the run loop when the callback is
    // destroyed.
    ASSERT_THAT(pipe_stream.ConnectNamedPipe(base::BindLambdaForTesting(
                    [loop_quitter = base::ScopedClosureRunner(QuitClosure())](
                        int error) { FAIL(); })),
                IsError(ERR_IO_PENDING));

    // Delete the FileStream; thereby cancelling the pending IO operation.
  }

  // Pump messages until the callback is destroyed following cancellation. The
  // context is still alive at this point, as a task to close the file has been
  // posted to the stream's task runner.
  RunUntilQuit();

  // Pump messages again until the task to close the file and delete the context
  // runs.
  RunUntilIdle();
}

using FileStreamPipeDeathTest = FileStreamPipeTest;

// Tests that FileStream crashes if ConnectNamedPipe() is called for a normal
// file.
TEST_F(FileStreamPipeDeathTest, CannotConnectFile) {
  const base::FilePath exe_path(base::PathService::CheckedGet(base::FILE_EXE));
  base::File exe_file(exe_path, base::File::FLAG_OPEN | base::File::FLAG_READ |
                                    base::File::FLAG_ASYNC |
                                    base::File::FLAG_WIN_SHARE_DELETE);
  ASSERT_TRUE(exe_file.IsValid())
      << base::File::ErrorToString(exe_file.error_details());

  // Pass that file to a FileStream.
  FileStream file_stream(std::move(exe_file),
                         base::SingleThreadTaskRunner::GetCurrentDefault());
  ASSERT_TRUE(file_stream.IsOpen());

  ASSERT_CHECK_DEATH(
      { file_stream.ConnectNamedPipe(CompletionOnceCallback()); });
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace

}  // namespace net