[libc-commits] [libc] [libc] Implement fgetpos/fsetpos (PR #221349)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Fri Oct 9 04:04:03 PDT 2026


================
@@ -0,0 +1,308 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 fgetpos
+///
+//===----------------------------------------------------------------------===//
+
+#include "hdr/stdio_macros.h"
+#include "hdr/types/fpos_t.h"
+#include "hdr/types/wint_t.h"
+#include "src/__support/CPP/scope.h"
+#include "src/stdio/fclose.h"
+#include "src/stdio/feof.h"
+#include "src/stdio/ferror.h"
+#include "src/stdio/fgetpos.h"
+#include "src/stdio/fopen.h"
+#include "src/stdio/fread.h"
+#include "src/stdio/fseek.h"
+#include "src/stdio/fwrite.h"
+#include "src/stdio/setvbuf.h"
+#include "src/stdio/ungetc.h"
+#include "src/wchar/fgetwc.h"
+#include "src/wchar/fputwc.h"
+#include "src/wchar/fwide.h"
+#include "src/wchar/mbsinit.h"
+#include "src/wchar/ungetwc.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#include "test/UnitTest/Test.h"
+
+using LIBC_NAMESPACE::cpp::scope_exit;
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails;
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
+
+class LlvmLibcFgetposTest : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {
+protected:
+  void test_with_bufmode(int bufmode) {
+    constexpr char FILENAME[] = "testdata/fgetpos_bufmode.test";
+    auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+    constexpr size_t BUFFER_SIZE = 1024;
+    char buffer[BUFFER_SIZE];
+
+    ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "w+");
+    ASSERT_FALSE(file == nullptr);
+    scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+    ASSERT_EQ(0, LIBC_NAMESPACE::setvbuf(file, buffer, bufmode, BUFFER_SIZE));
+
+    constexpr char CONTENT[] = "12\n345\n6789";
+    constexpr size_t WRITE_SIZE = sizeof(CONTENT) - 1;
+    ASSERT_THAT(LIBC_NAMESPACE::fwrite(CONTENT, 1, WRITE_SIZE, file),
+                Succeeds(WRITE_SIZE));
+
+    fpos_t pos;
+    ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+    ASSERT_EQ(pos.pos, off_t(WRITE_SIZE));
+
+    ASSERT_THAT(LIBC_NAMESPACE::fseek(file, 0, SEEK_SET), Succeeds(0));
+    constexpr size_t READ_SIZE = WRITE_SIZE / 2;
+    char data[READ_SIZE];
+    ASSERT_THAT(LIBC_NAMESPACE::fread(data, 1, READ_SIZE, file),
+                Succeeds(READ_SIZE));
+    ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+    ASSERT_EQ(pos.pos, off_t(READ_SIZE));
+  }
+};
+
+TEST_F(LlvmLibcFgetposTest, NewlyOpenedFile) {
+  constexpr char FILENAME[] = "testdata/fgetpos_newly_opened.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "w+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  fpos_t pos;
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(0));
+}
+
+TEST_F(LlvmLibcFgetposTest, WriteAndRead) {
+  constexpr char FILENAME[] = "testdata/fgetpos_write_read.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "w+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  constexpr char FIRST_DATA[] = "1234567890"; // 10 bytes
+  constexpr size_t FIRST_SIZE = sizeof(FIRST_DATA) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(FIRST_DATA, 1, FIRST_SIZE, file),
+              Succeeds(FIRST_SIZE));
+
+  fpos_t pos;
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(FIRST_SIZE));
+
+  constexpr char SECOND_DATA[] = "abcde"; // 5 bytes
+  constexpr size_t SECOND_SIZE = sizeof(SECOND_DATA) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(SECOND_DATA, 1, SECOND_SIZE, file),
+              Succeeds(SECOND_SIZE));
+
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(FIRST_SIZE + SECOND_SIZE));
+
+  // Seek to offset 4
+  ASSERT_THAT(LIBC_NAMESPACE::fseek(file, 4, SEEK_SET), Succeeds(0));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(4));
+
+  // Read 3 bytes
+  char read_buf[4];
+  ASSERT_THAT(LIBC_NAMESPACE::fread(read_buf, 1, 3, file), Succeeds(size_t(3)));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(7));
+}
+
+TEST_F(LlvmLibcFgetposTest, UngetcEffectBinaryStream) {
+  constexpr char FILENAME[] = "testdata/fgetpos_ungetc_bin.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  // ISO C §7.21.7.10 guarantees decrement by 1 specifically for binary streams.
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "wb+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  constexpr char DATA[] = "abcdef";
+  constexpr size_t DATA_SIZE = sizeof(DATA) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(DATA, 1, DATA_SIZE, file),
+              Succeeds(DATA_SIZE));
+
+  // Rewind to beginning
+  ASSERT_THAT(LIBC_NAMESPACE::fseek(file, 0, SEEK_SET), Succeeds(0));
+
+  char read_buf[4];
+  ASSERT_THAT(LIBC_NAMESPACE::fread(read_buf, 1, 3, file), Succeeds(size_t(3)));
+
+  fpos_t pos;
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(3));
+
+  // Pushing back a character decrements the file position indicator by 1
+  ASSERT_EQ(LIBC_NAMESPACE::ungetc(read_buf[2], file), int(read_buf[2]));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(2));
+
+  // Reading back the character restores position
+  char c;
+  ASSERT_THAT(LIBC_NAMESPACE::fread(&c, 1, 1, file), Succeeds(size_t(1)));
+  ASSERT_EQ(c, read_buf[2]);
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(3));
+}
+
+TEST_F(LlvmLibcFgetposTest, WideStream) {
+  constexpr char FILENAME[] = "testdata/fgetpos_wide.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "w+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  // Orient stream to wide
+  ASSERT_GT(LIBC_NAMESPACE::fwide(file, 1), 0);
+
+  fpos_t pos;
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(0));
+  // Verify initial parse state is recorded
+  ASSERT_NE(LIBC_NAMESPACE::mbsinit(&pos.state), 0);
+
+  // Write ASCII wide char (1 byte in UTF-8)
+  ASSERT_EQ(LIBC_NAMESPACE::fputwc(L'A', file), static_cast<wint_t>(L'A'));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(1));
+
+  // Write multi-byte wide char: L'¢' (2 bytes in UTF-8: 0xC2, 0xA2)
+  ASSERT_EQ(LIBC_NAMESPACE::fputwc(L'¢', file), static_cast<wint_t>(L'¢'));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(3));
+
+  // Seek to start
+  ASSERT_THAT(LIBC_NAMESPACE::fseek(file, 0, SEEK_SET), Succeeds(0));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(0));
+
+  // Read back first wide char
+  ASSERT_EQ(LIBC_NAMESPACE::fgetwc(file), static_cast<wint_t>(L'A'));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(1));
+
+  // Read back second wide char (multi-byte)
+  ASSERT_EQ(LIBC_NAMESPACE::fgetwc(file), static_cast<wint_t>(L'¢'));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(3));
+
+  // Push back wide character and then read back to verify restoration
+  ASSERT_EQ(LIBC_NAMESPACE::ungetwc(L'¢', file), static_cast<wint_t>(L'¢'));
+  ASSERT_EQ(LIBC_NAMESPACE::fgetwc(file), static_cast<wint_t>(L'¢'));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(3));
+}
+
+TEST_F(LlvmLibcFgetposTest, AtEOF) {
+  constexpr char FILENAME[] = "testdata/fgetpos_eof.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "w+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  constexpr char DATA[] = "hello";
+  constexpr size_t DATA_SIZE = sizeof(DATA) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(DATA, 1, DATA_SIZE, file),
+              Succeeds(DATA_SIZE));
+
+  ASSERT_THAT(LIBC_NAMESPACE::fseek(file, 0, SEEK_SET), Succeeds(0));
+
+  // Reading past end in one call sets EOF indicator with non-zero read_limit
+  char buf[DATA_SIZE + 1];
+  ASSERT_EQ(LIBC_NAMESPACE::fread(buf, 1, DATA_SIZE + 1, file), DATA_SIZE);
+  ASSERT_NE(LIBC_NAMESPACE::feof(file), 0);
+
+  // fgetpos at EOF must succeed, report file size, and preserve EOF indicator
+  fpos_t pos;
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(DATA_SIZE));
+  ASSERT_NE(LIBC_NAMESPACE::feof(file), 0);
+
+  // ISO C allows switching from input to output without an intervening seek
+  // if the input operation encountered end-of-file.
+  constexpr char EXTRA[] = "!";
+  constexpr size_t EXTRA_SIZE = sizeof(EXTRA) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(EXTRA, 1, EXTRA_SIZE, file),
+              Succeeds(EXTRA_SIZE));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(DATA_SIZE + EXTRA_SIZE));
+}
+
+TEST_F(LlvmLibcFgetposTest, AppendMode) {
+  constexpr char FILENAME[] = "testdata/fgetpos_append.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  ::FILE *init_file = LIBC_NAMESPACE::fopen(FILEPATH, "w");
+  ASSERT_FALSE(init_file == nullptr);
+  ASSERT_EQ(0, LIBC_NAMESPACE::fclose(init_file));
+
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "ab+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  constexpr char FIRST[] = "12345";
+  constexpr size_t FIRST_SIZE = sizeof(FIRST) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(FIRST, 1, FIRST_SIZE, file),
+              Succeeds(FIRST_SIZE));
+
+  fpos_t pos;
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(FIRST_SIZE));
+
+  // Seek to beginning
+  ASSERT_THAT(LIBC_NAMESPACE::fseek(file, 0, SEEK_SET), Succeeds(0));
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(0));
+
+  // In append mode, write is always positioned at EOF
+  constexpr char SECOND[] = "67890";
+  constexpr size_t SECOND_SIZE = sizeof(SECOND) - 1;
+  ASSERT_THAT(LIBC_NAMESPACE::fwrite(SECOND, 1, SECOND_SIZE, file),
+              Succeeds(SECOND_SIZE));
+
+  ASSERT_THAT(LIBC_NAMESPACE::fgetpos(file, &pos), Succeeds(0));
+  ASSERT_EQ(pos.pos, off_t(FIRST_SIZE + SECOND_SIZE));
+}
+
+TEST_F(LlvmLibcFgetposTest, ErrnoPreservedOnSuccess) {
+  constexpr char FILENAME[] = "testdata/fgetpos_errno.test";
+  auto FILEPATH = libc_make_test_file_path(FILENAME);
+
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILEPATH, "w+");
+  ASSERT_FALSE(file == nullptr);
+  scope_exit close_file([&] { ASSERT_EQ(0, LIBC_NAMESPACE::fclose(file)); });
+
+  // Pre-set errno to non-zero value
+  libc_errno = 42;
+
+  fpos_t pos;
+  ASSERT_EQ(LIBC_NAMESPACE::fgetpos(file, &pos), 0);
+  ASSERT_EQ(pos.pos, off_t(0));
+  // ISO C / POSIX: errno must remain unaltered on success
+  ASSERT_EQ(static_cast<int>(libc_errno), 42);
----------------
kaladron wrote:

ASSERT_ERRNO_EQ(42).

This should also cleanup libc_errno and make the reset below unneeded.

https://github.com/llvm/llvm-project/pull/221349


More information about the libc-commits mailing list