[libc-commits] [libc] [libc] implement fgetws (PR #196161)

Michael Jones via libc-commits libc-commits at lists.llvm.org
Wed May 6 12:55:05 PDT 2026


https://github.com/michaelrj-google created https://github.com/llvm/llvm-project/pull/196161

Add fgetws function and tests. Part 5/11. All build file changes are in
part 11.


>From 605c341eafe5a620cf4b175d58326675b621c93d Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Wed, 6 May 2026 18:27:43 +0000
Subject: [PATCH] [libc] implement fgetws

Add fgetws function and tests. Part 5/11. All build file changes are in
part 11.
---
 libc/src/stdio/generic/fgets.cpp    |   2 +
 libc/src/wchar/fgetws.cpp           |  63 ++++++++++
 libc/src/wchar/fgetws.h             |  21 ++++
 libc/test/src/wchar/fgetws_test.cpp | 181 ++++++++++++++++++++++++++++
 4 files changed, 267 insertions(+)
 create mode 100644 libc/src/wchar/fgetws.cpp
 create mode 100644 libc/src/wchar/fgetws.h
 create mode 100644 libc/test/src/wchar/fgetws_test.cpp

diff --git a/libc/src/stdio/generic/fgets.cpp b/libc/src/stdio/generic/fgets.cpp
index 33d469c620ca2..e3de77b5dc281 100644
--- a/libc/src/stdio/generic/fgets.cpp
+++ b/libc/src/stdio/generic/fgets.cpp
@@ -29,6 +29,8 @@ LLVM_LIBC_FUNCTION(char *, fgets,
   // i is an int because it's frequently compared to count, which is also int.
   int i = 0;
 
+  // TODO: rewrite this logic. If there's an error it should return nullptr. See
+  // fgetws for example.
   for (; i < (count - 1) && c != '\n'; ++i) {
     auto result = stream->read_unlocked(&c, 1);
     size_t r = result.value;
diff --git a/libc/src/wchar/fgetws.cpp b/libc/src/wchar/fgetws.cpp
new file mode 100644
index 0000000000000..97031f9af144f
--- /dev/null
+++ b/libc/src/wchar/fgetws.cpp
@@ -0,0 +1,63 @@
+//===-- Implementation of fgetws ------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/wchar/fgetws.h"
+#include "hdr/types/FILE.h"
+#include "src/__support/File/file.h"
+#include "src/__support/common.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/macros/null_check.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(wchar_t *, fgetws,
+                   (wchar_t *__restrict ws, int count,
+                    ::FILE *__restrict stream)) {
+  if (count <= 0)
+    return nullptr;
+
+  LIBC_CRASH_ON_NULLPTR(ws);
+  LIBC_CRASH_ON_NULLPTR(stream);
+
+  auto *f = reinterpret_cast<File *>(stream);
+
+  if (count == 1) {
+    ws[0] = L'\0';
+    return ws;
+  }
+
+  f->lock();
+
+  wchar_t *result = ws;
+  int chars_read = 0;
+  wchar_t c = L'\0';
+
+  for (; chars_read < count - 1 && c != '\n'; ++chars_read) {
+    auto read_res = f->read_unlocked(&c, 1);
+    if (read_res.has_error()) {
+      libc_errno = read_res.error;
+      result = nullptr;
+      break;
+    }
+    if (read_res.value < 1) {
+      if (chars_read == 0)
+        result = nullptr;
+      break;
+    }
+    ws[chars_read] = c;
+  }
+
+  if (result != nullptr)
+    ws[chars_read] = L'\0';
+
+  f->unlock();
+  return result;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/wchar/fgetws.h b/libc/src/wchar/fgetws.h
new file mode 100644
index 0000000000000..5fe9572ae25f7
--- /dev/null
+++ b/libc/src/wchar/fgetws.h
@@ -0,0 +1,21 @@
+//===-- Implementation header for fgetws ----------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_WCHAR_FGETWS_H
+#define LLVM_LIBC_SRC_WCHAR_FGETWS_H
+
+#include "hdr/types/FILE.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+wchar_t *fgetws(wchar_t *__restrict str, int count, ::FILE *__restrict stream);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_WCHAR_FGETWS_H
diff --git a/libc/test/src/wchar/fgetws_test.cpp b/libc/test/src/wchar/fgetws_test.cpp
new file mode 100644
index 0000000000000..5bd6cd7dc9d98
--- /dev/null
+++ b/libc/test/src/wchar/fgetws_test.cpp
@@ -0,0 +1,181 @@
+//===-- Unittests for fgetws ----------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "hdr/errno_macros.h"
+#include "hdr/types/wint_t.h"
+#include "src/stdio/fclose.h"
+#include "src/stdio/ferror.h"
+#include "src/stdio/fopen.h"
+#include "src/stdio/fwrite.h"
+#include "src/wchar/fgetws.h"
+#include "src/wchar/fwide.h"
+#include "src/wchar/wcscmp.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/Test.h"
+
+using LlvmLibcFgetwsTest = LIBC_NAMESPACE::testing::ErrnoCheckingTest;
+
+// TODO: Refactor these tests to use standard wide-character string comparison
+// assert macros (e.g., EXPECT_STREQ for wchar_t) once they are supported
+// natively by the LLVM-libc test framework, instead of calling wcscmp.
+TEST_F(LlvmLibcFgetwsTest, ReadWideString) {
+  auto FILENAME =
+      libc_make_test_file_path(APPEND_LIBC_TEST("fgetws_string.test"));
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+  ASSERT_FALSE(file == nullptr);
+
+  // Write UTF-8 bytes for: "Hello, ¢€𐍈 world!\n"
+  constexpr unsigned char CONTENT[] = {
+      'H',  'e',  'l',  'l',  'o', ',', ' ', 0xC2, 0xA2, 0xE2, 0x82, 0xAC,
+      0xF0, 0x90, 0x8D, 0x88, ' ', 'w', 'o', 'r',  'l',  'd',  '!',  '\n'};
+  ASSERT_EQ(LIBC_NAMESPACE::fwrite(CONTENT, 1, sizeof(CONTENT), file),
+            sizeof(CONTENT));
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+
+  // Open for reading
+  file = LIBC_NAMESPACE::fopen(FILENAME, "r");
+  ASSERT_FALSE(file == nullptr);
+
+  wchar_t buffer[50] = {0};
+  wchar_t *result = LIBC_NAMESPACE::fgetws(buffer, 50, file);
+  ASSERT_FALSE(result == nullptr);
+  EXPECT_EQ(result, buffer);
+  EXPECT_EQ(LIBC_NAMESPACE::wcscmp(buffer, L"Hello, ¢€𐍈 world!\n"), 0);
+
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+}
+
+TEST_F(LlvmLibcFgetwsTest, ReadBounded) {
+  auto FILENAME =
+      libc_make_test_file_path(APPEND_LIBC_TEST("fgetws_bounded.test"));
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+  ASSERT_FALSE(file == nullptr);
+
+  // Write "1234567890"
+  constexpr char CONTENT[] = "1234567890";
+  ASSERT_EQ(LIBC_NAMESPACE::fwrite(CONTENT, 1, sizeof(CONTENT) - 1, file),
+            sizeof(CONTENT) - 1);
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+
+  // Open for reading
+  file = LIBC_NAMESPACE::fopen(FILENAME, "r");
+  ASSERT_FALSE(file == nullptr);
+
+  wchar_t buffer[10] = {0};
+  // Read bounded by count = 5 (4 chars + null terminator)
+  wchar_t *result = LIBC_NAMESPACE::fgetws(buffer, 5, file);
+  ASSERT_FALSE(result == nullptr);
+  EXPECT_EQ(result, buffer);
+  EXPECT_EQ(LIBC_NAMESPACE::wcscmp(buffer, L"1234"), 0);
+
+  // Read bounded by count = 1 (writes only null terminator)
+  wchar_t buffer_one[5] = {L'x', L'y', L'z', L'\0'};
+  wchar_t *result_one = LIBC_NAMESPACE::fgetws(buffer_one, 1, file);
+  ASSERT_FALSE(result_one == nullptr);
+  EXPECT_EQ(result_one, buffer_one);
+  EXPECT_EQ(static_cast<wint_t>(buffer_one[0]), static_cast<wint_t>(L'\0'));
+  EXPECT_EQ(static_cast<wint_t>(buffer_one[1]),
+            static_cast<wint_t>(L'y')); // untouched
+
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+}
+
+TEST_F(LlvmLibcFgetwsTest, NewlineStops) {
+  auto FILENAME =
+      libc_make_test_file_path(APPEND_LIBC_TEST("fgetws_newline.test"));
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+  ASSERT_FALSE(file == nullptr);
+
+  // Write "abc\ndef"
+  constexpr char CONTENT[] = "abc\ndef";
+  ASSERT_EQ(LIBC_NAMESPACE::fwrite(CONTENT, 1, sizeof(CONTENT) - 1, file),
+            sizeof(CONTENT) - 1);
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+
+  // Open for reading
+  file = LIBC_NAMESPACE::fopen(FILENAME, "r");
+  ASSERT_FALSE(file == nullptr);
+
+  wchar_t buffer[20] = {0};
+  wchar_t *result = LIBC_NAMESPACE::fgetws(buffer, 10, file);
+  ASSERT_FALSE(result == nullptr);
+  EXPECT_EQ(result, buffer);
+  EXPECT_EQ(LIBC_NAMESPACE::wcscmp(buffer, L"abc\n"), 0);
+
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+}
+
+TEST_F(LlvmLibcFgetwsTest, InvalidStream) {
+  auto FILENAME =
+      libc_make_test_file_path(APPEND_LIBC_TEST("fgetws_invalid.test"));
+
+  // Create the file first
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+  ASSERT_FALSE(file == nullptr);
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+
+  // Open in write-only mode
+  file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+  ASSERT_FALSE(file == nullptr);
+
+  // Try to read from write-only stream
+  wchar_t buffer[10] = {0};
+  ASSERT_ERRNO_SUCCESS();
+  EXPECT_EQ(LIBC_NAMESPACE::fgetws(buffer, 5, file),
+            static_cast<wchar_t *>(nullptr));
+  ASSERT_ERRNO_EQ(EBADF);
+  EXPECT_NE(LIBC_NAMESPACE::ferror(file), 0);
+
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+}
+
+TEST_F(LlvmLibcFgetwsTest, EncodingErrorEILSEQ) {
+  auto FILENAME =
+      libc_make_test_file_path(APPEND_LIBC_TEST("fgetws_eilseq.test"));
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w");
+  ASSERT_FALSE(file == nullptr);
+
+  // Write an invalid UTF-8 sequence: 0x80 (stray continuation byte)
+  constexpr unsigned char CONTENT[] = {0x80};
+  ASSERT_EQ(LIBC_NAMESPACE::fwrite(CONTENT, 1, sizeof(CONTENT), file),
+            sizeof(CONTENT));
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+
+  // Open for reading
+  file = LIBC_NAMESPACE::fopen(FILENAME, "r");
+  ASSERT_FALSE(file == nullptr);
+
+  // Reading invalid sequence should fail with EILSEQ
+  wchar_t buffer[10] = {0};
+  ASSERT_ERRNO_SUCCESS();
+  EXPECT_EQ(LIBC_NAMESPACE::fgetws(buffer, 5, file),
+            static_cast<wchar_t *>(nullptr));
+  ASSERT_ERRNO_EQ(EILSEQ);
+  EXPECT_NE(LIBC_NAMESPACE::ferror(file), 0);
+
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+}
+
+TEST_F(LlvmLibcFgetwsTest, ByteModeFailure) {
+  auto FILENAME =
+      libc_make_test_file_path(APPEND_LIBC_TEST("fgetws_bytemode.test"));
+  ::FILE *file = LIBC_NAMESPACE::fopen(FILENAME, "w+");
+  ASSERT_FALSE(file == nullptr);
+
+  // Orient to byte mode
+  EXPECT_LT(LIBC_NAMESPACE::fwide(file, -1), 0);
+
+  // Read wide string should fail and set errno to EINVAL
+  wchar_t buffer[10] = {0};
+  EXPECT_EQ(LIBC_NAMESPACE::fgetws(buffer, 5, file),
+            static_cast<wchar_t *>(nullptr));
+  ASSERT_ERRNO_EQ(EINVAL);
+  EXPECT_NE(LIBC_NAMESPACE::ferror(file), 0);
+
+  ASSERT_EQ(LIBC_NAMESPACE::fclose(file), 0);
+}



More information about the libc-commits mailing list