Add some tests for file and fd BIOs

We had almost no test coverage for this. I was previewing a change and
made a mistake which was caught surprisingly late.

In doing so, this flags that BIO_write on file BIOs returns something
inconsistent with everything else.

Bug: 42290372
Change-Id: Ib541386fda2b6b5599e7281fb133fce446a58b61
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/96047
Auto-Submit: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/bio/bio_test.cc b/crypto/bio/bio_test.cc
index 861eb2c..3c01952 100644
--- a/crypto/bio/bio_test.cc
+++ b/crypto/bio/bio_test.cc
@@ -55,6 +55,7 @@
 static std::string LastSocketError() { return strerror(errno); }
 static const int kOpenReadOnlyBinary = O_RDONLY;
 static const int kOpenReadOnlyText = O_RDONLY;
+static const int kOpenWriteOnlyBinary = O_WRONLY;
 #else
 using Socket = SOCKET;
 static std::string LastSocketError() {
@@ -63,7 +64,8 @@
   return buf;
 }
 static const int kOpenReadOnlyBinary = _O_RDONLY | _O_BINARY;
-static const int kOpenReadOnlyText = O_RDONLY | _O_TEXT;
+static const int kOpenReadOnlyText = _O_RDONLY | _O_TEXT;
+static const int kOpenWriteOnlyBinary = _O_WRONLY | _O_BINARY;
 #endif
 
 class OwnedSocket {
@@ -946,6 +948,120 @@
   expect_text_mode(bio.get());
 }
 
+// Test basic I/O on file and fd BIOs.
+TEST(BIOTest, FileIO) {
+  if (SkipTempFileTests()) {
+    GTEST_SKIP();
+  }
+
+  for (bool use_fd : {false, true}) {
+    SCOPED_TRACE(use_fd);
+
+    TemporaryFile temp;
+    ASSERT_TRUE(temp.Init());
+
+    // Open the file writable.
+    UniquePtr<BIO> bio;
+    ScopedFD fd;
+    if (use_fd) {
+      fd = temp.OpenFD(kOpenWriteOnlyBinary);
+      ASSERT_TRUE(fd.is_valid());
+      bio.reset(BIO_new_fd(fd.get(), BIO_NOCLOSE));
+      ASSERT_TRUE(bio);
+    } else {
+      bio.reset(BIO_new_file(temp.path().c_str(), "wb"));
+      ASSERT_TRUE(bio);
+    }
+
+    // Write something to the file with BIO APIs.
+    EXPECT_EQ(BIO_tell(bio.get()), 0);
+    EXPECT_EQ(BIO_write(bio.get(), "hello world", 11), 11);
+    EXPECT_EQ(BIO_tell(bio.get()), 11);
+
+    // Open the file readable.
+    bio = nullptr;
+    if (use_fd) {
+      fd = temp.OpenFD(kOpenReadOnlyBinary);
+      ASSERT_TRUE(fd.is_valid());
+      bio.reset(BIO_new_fd(fd.get(), BIO_NOCLOSE));
+      ASSERT_TRUE(bio);
+    } else {
+      bio.reset(BIO_new_file(temp.path().c_str(), "rb"));
+      ASSERT_TRUE(bio);
+    }
+
+    // Seek to "world". Note |BIO_seek|'s return values are wildly inconsistent
+    // between BIOs.
+    EXPECT_EQ(BIO_tell(bio.get()), 0);
+    if (use_fd) {
+      EXPECT_EQ(BIO_seek(bio.get(), 6), 6);
+    } else {
+      EXPECT_EQ(BIO_seek(bio.get(), 6), 0);
+    }
+    EXPECT_EQ(BIO_tell(bio.get()), 6);
+
+    // Read the data in three parts, to test full, partial, and EOF reads.
+    char buf[3];
+    EXPECT_EQ(BIO_read(bio.get(), buf, 3), 3);
+    EXPECT_EQ(Bytes(buf, 3), Bytes("wor"));
+    EXPECT_EQ(BIO_read(bio.get(), buf, 3), 2);
+    EXPECT_EQ(Bytes(buf, 2), Bytes("ld"));
+    EXPECT_EQ(BIO_read(bio.get(), buf, 3), 0);
+    EXPECT_EQ(BIO_tell(bio.get()), 11);
+  }
+}
+
+// Test that file and fd BIOs correctly handle errors. Simulate errors by trying
+// to write to an unreadable handle and vice versa.
+TEST(BIOTest, FileFDError) {
+  if (SkipTempFileTests()) {
+    GTEST_SKIP();
+  }
+
+  TemporaryFile temp;
+  ASSERT_TRUE(temp.Init());
+
+  // File write error.
+  {
+    UniquePtr<BIO> bio(BIO_new_file(temp.path().c_str(), "rb"));
+    ASSERT_TRUE(bio);
+    // TODO(crbug.com/42290372): File BIOs currently return zero instead of -1
+    // on write error.
+    EXPECT_EQ(BIO_write(bio.get(), "foo", 3), 0);
+    EXPECT_FALSE(BIO_should_retry(bio.get()));
+  }
+
+  // FD write error.
+  {
+    ScopedFD fd = temp.OpenFD(kOpenReadOnlyBinary);
+    ASSERT_TRUE(fd.is_valid());
+    UniquePtr<BIO> bio(BIO_new_fd(fd.get(), BIO_NOCLOSE));
+    ASSERT_TRUE(bio);
+    EXPECT_EQ(BIO_write(bio.get(), "foo", 3), -1);
+    EXPECT_FALSE(BIO_should_retry(bio.get()));
+  }
+
+  // File read error.
+  {
+    UniquePtr<BIO> bio(BIO_new_file(temp.path().c_str(), "wb"));
+    ASSERT_TRUE(bio);
+    char buf[3];
+    EXPECT_EQ(BIO_read(bio.get(), buf, sizeof(buf)), -1);
+    EXPECT_FALSE(BIO_should_retry(bio.get()));
+  }
+
+  // FD read error.
+  {
+    ScopedFD fd = temp.OpenFD(kOpenWriteOnlyBinary);
+    ASSERT_TRUE(fd.is_valid());
+    UniquePtr<BIO> bio(BIO_new_fd(fd.get(), BIO_NOCLOSE));
+    ASSERT_TRUE(bio);
+    char buf[3];
+    EXPECT_EQ(BIO_read(bio.get(), buf, sizeof(buf)), -1);
+    EXPECT_FALSE(BIO_should_retry(bio.get()));
+  }
+}
+
 // Run through the tests twice, swapping |bio1| and |bio2|, for symmetry.
 class BIOPairTest : public testing::TestWithParam<bool> {};