| //===----------------------------------------------------------------------===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| /// | |
| /// \file | |
| /// Unittests for freopen. | |
| /// | |
| //===----------------------------------------------------------------------===// | |
| #include "src/fcntl/fcntl.h" | |
| #include "src/stdio/clearerr.h" | |
| #include "src/stdio/fclose.h" | |
| #include "src/stdio/feof.h" | |
| #include "src/stdio/ferror.h" | |
| #include "src/stdio/fflush.h" | |
| #include "src/stdio/fileno.h" | |
| #include "src/stdio/fopen.h" | |
| #include "src/stdio/fread.h" | |
| #include "src/stdio/freopen.h" | |
| #include "src/stdio/fwrite.h" | |
| #include "src/stdio/stdout.h" | |
| #include "src/unistd/close.h" | |
| #include "src/wchar/fwide.h" | |
| #include "test/UnitTest/ErrnoCheckingTest.h" | |
| #include "test/UnitTest/ErrnoSetterMatcher.h" | |
| #include "test/UnitTest/Test.h" | |
| #include "hdr/fcntl_macros.h" | |
| #include "hdr/stdio_macros.h" | |
| #include "src/__support/macros/properties/os.h" | |
| using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails; | |
| using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds; | |
| struct LlvmLibcFreopenTest : public LIBC_NAMESPACE::testing::ErrnoCheckingTest { | |
| static constexpr size_t VERIFY_BUFFER_SIZE = 128; | |
| void verify_file_content(const char *filepath, const char *expected) { | |
| size_t len = 0; | |
| while (expected[len] != '\0') | |
| ++len; | |
| ASSERT_LT(len, VERIFY_BUFFER_SIZE); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(filepath, "r"); | |
| ASSERT_FALSE(file == nullptr); | |
| char read_buf[VERIFY_BUFFER_SIZE] = {0}; | |
| ASSERT_THAT( | |
| LIBC_NAMESPACE::fread(read_buf, 1, VERIFY_BUFFER_SIZE - 1, file), | |
| Succeeds(len)); | |
| ASSERT_STREQ(read_buf, expected); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); | |
| } | |
| }; | |
| TEST_F(LlvmLibcFreopenTest, ReopenFile) { | |
| const auto FILENAME_A = libc_make_test_file_path("freopen_a.test"); | |
| const auto FILENAME_B = libc_make_test_file_path("freopen_b.test"); | |
| // Step 1: Open file A and write initial content. | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME_A, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| constexpr char CONTENT_A[] = "File A Content"; | |
| ASSERT_THAT(LIBC_NAMESPACE::fwrite(CONTENT_A, 1, sizeof(CONTENT_A) - 1, file), | |
| Succeeds(sizeof(CONTENT_A) - 1)); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); | |
| // Step 2: Open file A for reading. | |
| file = LIBC_NAMESPACE::fopen(FILENAME_A, "r"); | |
| ASSERT_FALSE(file == nullptr); | |
| // Step 3: Use freopen to redirect stream from file A to file B for writing. | |
| ::FILE *reopened_file = LIBC_NAMESPACE::freopen(FILENAME_B, "w", file); | |
| ASSERT_EQ(reopened_file, file); | |
| // Step 4: Write to reopened stream (file B). | |
| constexpr char CONTENT_B[] = "File B Content Written via freopen"; | |
| ASSERT_THAT(LIBC_NAMESPACE::fwrite(CONTENT_B, 1, sizeof(CONTENT_B) - 1, | |
| reopened_file), | |
| Succeeds(sizeof(CONTENT_B) - 1)); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(reopened_file), Succeeds(0)); | |
| // Step 5: Verify file B content. | |
| verify_file_content(FILENAME_B, CONTENT_B); | |
| } | |
| TEST_F(LlvmLibcFreopenTest, NullFilenameModeChange) { | |
| const auto FILENAME = libc_make_test_file_path("freopen_null_filename.test"); | |
| // Step 1: Open file with write-update mode. | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w+"); | |
| ASSERT_FALSE(file == nullptr); | |
| constexpr char INITIAL_CONTENT[] = "Initial Data "; | |
| ASSERT_THAT(LIBC_NAMESPACE::fwrite(INITIAL_CONTENT, 1, | |
| sizeof(INITIAL_CONTENT) - 1, file), | |
| Succeeds(sizeof(INITIAL_CONTENT) - 1)); | |
| // Step 2: Change mode with filename == nullptr to append. | |
| ::FILE *reopened = LIBC_NAMESPACE::freopen(nullptr, "a", file); | |
| ASSERT_EQ(reopened, file); | |
| // Step 3: Write appended content. | |
| constexpr char APPENDED_CONTENT[] = "Appended Data"; | |
| ASSERT_THAT(LIBC_NAMESPACE::fwrite(APPENDED_CONTENT, 1, | |
| sizeof(APPENDED_CONTENT) - 1, reopened), | |
| Succeeds(sizeof(APPENDED_CONTENT) - 1)); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(reopened), Succeeds(0)); | |
| // Step 4: Verify combined content. | |
| verify_file_content(FILENAME, "Initial Data Appended Data"); | |
| } | |
| TEST_F(LlvmLibcFreopenTest, NullFilenameInvalidModeChange) { | |
| const auto FILENAME = | |
| libc_make_test_file_path("freopen_invalid_mode_change.test"); | |
| // Open file read-only. | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); | |
| file = LIBC_NAMESPACE::fopen(FILENAME, "r"); | |
| ASSERT_FALSE(file == nullptr); | |
| // Attempt incompatible mode change (r to w with filename == nullptr). | |
| ASSERT_THAT(LIBC_NAMESPACE::freopen(nullptr, "w", file), | |
| Fails(EBADF, static_cast<void *>(nullptr))); | |
| // Attempt incompatible mode change (r to w+ with filename == nullptr). | |
| ASSERT_THAT(LIBC_NAMESPACE::freopen(nullptr, "w+", file), | |
| Fails(EBADF, static_cast<void *>(nullptr))); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); | |
| } | |
| #if defined(LIBC_TARGET_OS_IS_POSIX) || defined(LIBC_TARGET_OS_IS_LINUX) | |
| TEST_F(LlvmLibcFreopenTest, InvalidModeFailure) { | |
| const auto FILENAME = libc_make_test_file_path("freopen_invalid_mode.test"); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| int old_fd = LIBC_NAMESPACE::fileno(file); | |
| ASSERT_GE(old_fd, 0); | |
| ASSERT_THAT(LIBC_NAMESPACE::freopen(FILENAME, "invalid_mode_str", file), | |
| Fails(EINVAL, static_cast<void *>(nullptr))); | |
| // Per POSIX spec, original stream fd was closed on filename != nullptr | |
| // freopen attempt. | |
| ASSERT_THAT(LIBC_NAMESPACE::fcntl(old_fd, F_GETFL), Fails(EBADF, -1)); | |
| // Clean up stream object to avoid memory leaks. | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Fails(EBADF, EOF)); | |
| } | |
| #endif | |
| #if defined(LIBC_TARGET_OS_IS_POSIX) || defined(LIBC_TARGET_OS_IS_LINUX) | |
| TEST_F(LlvmLibcFreopenTest, NonExistentFileFailure) { | |
| auto EXISTING_FILE = libc_make_test_file_path("freopen_existing.test"); | |
| auto NON_EXISTENT_FILE = | |
| libc_make_test_file_path("freopen_does_not_exist.test"); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(EXISTING_FILE, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| int old_fd = LIBC_NAMESPACE::fileno(file); | |
| ASSERT_GT(old_fd, 0); | |
| // Attempt to freopen a non-existent file in read mode. | |
| ASSERT_THAT(LIBC_NAMESPACE::freopen(NON_EXISTENT_FILE, "r", file), | |
| Fails(ENOENT, static_cast<void *>(nullptr))); | |
| // Per POSIX spec: The original stream fd is closed even if open fails. | |
| ASSERT_THAT(LIBC_NAMESPACE::fcntl(old_fd, F_GETFL), Fails(EBADF, -1)); | |
| // Clean up stream object to avoid memory leaks. | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Fails(EBADF, EOF)); | |
| } | |
| #endif // LIBC_TARGET_OS_IS_POSIX || LIBC_TARGET_OS_IS_LINUX | |
| TEST_F(LlvmLibcFreopenTest, FlushBeforeReopenTest) { | |
| const auto FILENAME_A = libc_make_test_file_path("freopen_flush_a.test"); | |
| const auto FILENAME_B = libc_make_test_file_path("freopen_flush_b.test"); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME_A, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| constexpr char DIRTY_DATA[] = "Buffered data before freopen"; | |
| ASSERT_THAT( | |
| LIBC_NAMESPACE::fwrite(DIRTY_DATA, 1, sizeof(DIRTY_DATA) - 1, file), | |
| Succeeds(sizeof(DIRTY_DATA) - 1)); | |
| // freopen must flush unwritten buffered data to FILENAME_A before reopening | |
| ::FILE *reopened = LIBC_NAMESPACE::freopen(FILENAME_B, "w", file); | |
| ASSERT_EQ(reopened, file); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(reopened), Succeeds(0)); | |
| // Verify FILENAME_A received the flushed dirty buffer | |
| verify_file_content(FILENAME_A, DIRTY_DATA); | |
| } | |
| TEST_F(LlvmLibcFreopenTest, ClearFlagsTest) { | |
| const auto FILENAME_A = libc_make_test_file_path("freopen_flags_a.test"); | |
| const auto FILENAME_B = libc_make_test_file_path("freopen_flags_b.test"); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME_A, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| constexpr char SHORT_DATA[] = "X"; | |
| ASSERT_THAT(LIBC_NAMESPACE::fwrite(SHORT_DATA, 1, 1, file), | |
| Succeeds(static_cast<size_t>(1))); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Succeeds(0)); | |
| // Trigger EOF on file | |
| file = LIBC_NAMESPACE::fopen(FILENAME_A, "r"); | |
| ASSERT_FALSE(file == nullptr); | |
| char buf[4]; | |
| LIBC_NAMESPACE::fread(buf, 1, sizeof(buf), file); | |
| ASSERT_NE(LIBC_NAMESPACE::feof(file), 0); | |
| // freopen must clear EOF and error indicators | |
| ::FILE *reopened = LIBC_NAMESPACE::freopen(FILENAME_B, "w", file); | |
| ASSERT_EQ(reopened, file); | |
| ASSERT_EQ(LIBC_NAMESPACE::feof(reopened), 0); | |
| ASSERT_EQ(LIBC_NAMESPACE::ferror(reopened), 0); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(reopened), Succeeds(0)); | |
| } | |
| #if defined(LIBC_TARGET_OS_IS_POSIX) || defined(LIBC_TARGET_OS_IS_LINUX) | |
| TEST_F(LlvmLibcFreopenTest, NullFilenameBadFdTest) { | |
| const auto FILENAME = libc_make_test_file_path("freopen_bad_fd.test"); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| int fd = LIBC_NAMESPACE::fileno(file); | |
| ASSERT_GT(fd, 0); | |
| // Manually close underlying fd to simulate bad file descriptor state | |
| ASSERT_THAT(LIBC_NAMESPACE::close(fd), Succeeds(0)); | |
| // freopen with filename == nullptr on invalid fd should return nullptr + | |
| // EBADF | |
| ASSERT_THAT(LIBC_NAMESPACE::freopen(nullptr, "a", file), | |
| Fails(EBADF, static_cast<void *>(nullptr))); | |
| // Clean up stream object to avoid memory leaks. | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(file), Fails(EBADF, EOF)); | |
| } | |
| #endif // LIBC_TARGET_OS_IS_POSIX || LIBC_TARGET_OS_IS_LINUX | |
| TEST_F(LlvmLibcFreopenTest, ResetOrientationTest) { | |
| const auto FILENAME_A = libc_make_test_file_path("freopen_orient_a.test"); | |
| const auto FILENAME_B = libc_make_test_file_path("freopen_orient_b.test"); | |
| ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME_A, "w"); | |
| ASSERT_FALSE(file == nullptr); | |
| // Set wide orientation | |
| ASSERT_GT(LIBC_NAMESPACE::fwide(file, 1), 0); | |
| // freopen must reset orientation to 0 (unoriented) | |
| ::FILE *reopened = LIBC_NAMESPACE::freopen(FILENAME_B, "w", file); | |
| ASSERT_EQ(reopened, file); | |
| ASSERT_THAT(LIBC_NAMESPACE::fwide(reopened, 0), Succeeds(0)); | |
| ASSERT_THAT(LIBC_NAMESPACE::fclose(reopened), Succeeds(0)); | |
| } | |
| #if defined(LIBC_TARGET_OS_IS_POSIX) || defined(LIBC_TARGET_OS_IS_LINUX) | |
| TEST_F(LlvmLibcFreopenTest, StdoutRedirectionTest) { | |
| const auto FILENAME = libc_make_test_file_path("freopen_stdout.test"); | |
| int stdout_fd = LIBC_NAMESPACE::fileno(LIBC_NAMESPACE::stdout); | |
| ASSERT_EQ(stdout_fd, 1); | |
| // Redirect stdout to FILENAME | |
| ::FILE *reopened = | |
| LIBC_NAMESPACE::freopen(FILENAME, "w", LIBC_NAMESPACE::stdout); | |
| ASSERT_EQ(reopened, static_cast<::FILE *>(LIBC_NAMESPACE::stdout)); | |
| // Verify fileno(stdout) is preserved as 1 | |
| ASSERT_EQ(LIBC_NAMESPACE::fileno(LIBC_NAMESPACE::stdout), 1); | |
| constexpr char MSG[] = "Redirected Stdout"; | |
| ASSERT_THAT( | |
| LIBC_NAMESPACE::fwrite(MSG, 1, sizeof(MSG) - 1, LIBC_NAMESPACE::stdout), | |
| Succeeds(sizeof(MSG) - 1)); | |
| ASSERT_THAT(LIBC_NAMESPACE::fflush(LIBC_NAMESPACE::stdout), Succeeds(0)); | |
| // Verify file content | |
| verify_file_content(FILENAME, MSG); | |
| } | |
| TEST_F(LlvmLibcFreopenTest, StdoutFailureTest) { | |
| auto NON_EXISTENT_FILE = libc_make_test_file_path("freopen_stdout_fail.test"); | |
| // Attempt to freopen non-existent file on stdout. | |
| // This must return nullptr + ENOENT without attempting to free the static | |
| // stdout object. | |
| ASSERT_THAT( | |
| LIBC_NAMESPACE::freopen(NON_EXISTENT_FILE, "r", LIBC_NAMESPACE::stdout), | |
| Fails(ENOENT, static_cast<void *>(nullptr))); | |
| } | |
| #endif | |
| TEST_F(LlvmLibcFreopenTest, NullStreamFailure) { | |
| const auto FILENAME = libc_make_test_file_path("freopen_null_stream.test"); | |
| const char *fn = FILENAME; | |
| EXPECT_DEATH([=] { LIBC_NAMESPACE::freopen(fn, "r", nullptr); }, | |
| WITH_SIGNAL(-1)); | |
| } |