[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