[libcxx-commits] [libcxx] [libc++] Optimize filebuf::seekoff (PR #211788)

Nikolas Klauser via libcxx-commits libcxx-commits at lists.llvm.org
Tue Oct 13 05:49:03 PDT 2026


https://github.com/philnik777 updated https://github.com/llvm/llvm-project/pull/211788

>From f8c37f9ff21aa69059c7758753abdce296350846 Mon Sep 17 00:00:00 2001
From: Nikolas Klauser <nikolasklauser at berlin.de>
Date: Fri, 24 Jul 2026 14:21:07 +0200
Subject: [PATCH] [libc++] Optimize filebuf::seekoff

---
 libcxx/include/fstream                        | 118 ++++-
 .../test/benchmarks/streams/fstream.bench.cpp |  61 ++-
 .../fstreams/filebuf.virtuals/seekoff.dat     |   1 +
 .../filebuf.virtuals/seekoff.pass.cpp         | 432 ++++++++++++++++--
 libcxx/test/support/locale_facets.h           | 275 +++++++++++
 libcxx/test/support/module.modulemap          |   1 +
 6 files changed, 820 insertions(+), 68 deletions(-)
 create mode 100644 libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.dat
 create mode 100644 libcxx/test/support/locale_facets.h

diff --git a/libcxx/include/fstream b/libcxx/include/fstream
index f1792b12e229c5..8bd0feca8f200d 100644
--- a/libcxx/include/fstream
+++ b/libcxx/include/fstream
@@ -439,7 +439,7 @@ private:
   void __write_mode();
 
   _LIBCPP_HIDE_FROM_ABI static int __fseek(FILE* __file, pos_type __offset, int __whence);
-  _LIBCPP_HIDE_FROM_ABI static pos_type __ftell(FILE* __file);
+  _LIBCPP_HIDE_FROM_ABI static off_type __ftell(FILE* __file);
   _LIBCPP_HIDE_FROM_ABI static FILE* __fopen(const char* __name, const char* __mode);
 
   _LIBCPP_EXPORTED_FROM_ABI friend FILE* __get_ostream_file(ostream&);
@@ -490,6 +490,90 @@ private:
     }
     return traits_type::eof();
   }
+
+  _LIBCPP_HIDE_FROM_ABI pos_type __seek_from_begin(off_type __off, off_type __byte_off) {
+    if (__off < 0)
+      return pos_type(off_type(-1));
+
+    pos_type __success_pos = __off;
+    __success_pos.state(__st_);
+
+    // If we're not always noconv or have a write buffer, sync and seek.
+    // TODO: We don't have to sync() even if we're not always_noconv.
+    if (!__always_noconv_ || (__cm_ & ios::out)) {
+      if (sync() || __fseek(__file_, __byte_off, SEEK_SET))
+        return pos_type(off_type(-1));
+      return __success_pos;
+    }
+
+    off_type __file_pos    = __ftell(__file_);
+    auto __buffer_base_pos = __file_pos - (this->egptr() - this->eback());
+
+    // If the new position is within the buffer we can just move the current read pointer
+    if (__off >= __buffer_base_pos && __off <= __file_pos) {
+      this->setg(this->eback(), this->eback() + (__off - __buffer_base_pos), this->egptr());
+      return __success_pos;
+    } else {
+      // Otherwise drop the buffer and seek to the requested position in the underlying C stream. This is the expensive
+      // path, since it performs a syscall.
+      this->setg(nullptr, nullptr, nullptr);
+      __cm_ = 0;
+      if (__fseek(__file_, __byte_off, SEEK_SET))
+        return pos_type(off_type(-1));
+      return __success_pos;
+    }
+  }
+
+  _LIBCPP_HIDE_FROM_ABI pos_type __seek_from_current(off_type __off, off_type __byte_off) {
+    // If we're not always noconv or have a write buffer, sync and seek.
+    // TODO: We don't have to sync() even if we're not always_noconv.
+    // TODO: We don't have to sync() if `(__cm_ & ios::out) && __off == 0`.
+    if (!__always_noconv_ || (__cm_ & ios::out)) {
+      if (sync() || __fseek(__file_, __byte_off, SEEK_CUR))
+        return pos_type(off_type(-1));
+      pos_type __r = __ftell(__file_);
+      __r.state(__st_);
+      return __r;
+    }
+
+    // If the new position is within the buffer we can just move the current read pointer
+    if (__off < 0 ? this->gptr() - this->eback() <= __off : this->egptr() - this->gptr() >= __off) {
+      this->__gbump_ptrdiff(__off);
+
+      pos_type __ret = __ftell(__file_) - (this->egptr() - this->gptr());
+      __ret.state(__st_);
+      return __ret;
+    } else {
+      // Otherwise seek to the requested position in the underlying C stream and drop the buffer. This is the expensive
+      // path, since it performs a syscall.
+      if (__fseek(__file_, __byte_off - (this->egptr() - this->gptr()), SEEK_CUR))
+        return pos_type(off_type(-1));
+      this->setg(nullptr, nullptr, nullptr);
+      __cm_          = 0;
+      pos_type __ret = __ftell(__file_);
+      __ret.state(__st_);
+      return __ret;
+    }
+  }
+
+  _LIBCPP_HIDE_FROM_ABI pos_type __seek_from_end(off_type, off_type __byte_off) {
+    // If we're not always noconv or have a write buffer, sync and seek.
+    if (!__always_noconv_ || (__cm_ & ios::out)) {
+      if (sync() || __fseek(__file_, __byte_off, SEEK_END))
+        return pos_type(off_type(-1));
+      pos_type __r = __ftell(__file_);
+      __r.state(__st_);
+      return __r;
+    }
+
+    this->setg(nullptr, nullptr, nullptr);
+    __cm_ = 0;
+    if (__fseek(__file_, __byte_off, SEEK_END))
+      return pos_type(off_type(-1));
+    pos_type __ret = __ftell(__file_);
+    __ret.state(__st_);
+    return __ret;
+  }
 };
 
 template <class _CharT, class _Traits>
@@ -1057,28 +1141,20 @@ basic_filebuf<_CharT, _Traits>::seekoff(off_type __off, ios_base::seekdir __way,
     std::__throw_bad_cast();
 
   int __width = __cv_->encoding();
-  if (__file_ == nullptr || (__width <= 0 && __off != 0) || sync())
+  if (__file_ == nullptr || (__width <= 0 && __off != 0))
     return pos_type(off_type(-1));
-  // __width > 0 || __off == 0
-  int __whence;
+
+  auto __byte_off = __width <= 0 ? 0 : __width * __off;
+
   switch (__way) {
-  case ios_base::beg:
-    __whence = SEEK_SET;
-    break;
-  case ios_base::cur:
-    __whence = SEEK_CUR;
-    break;
-  case ios_base::end:
-    __whence = SEEK_END;
-    break;
-  default:
-    return pos_type(off_type(-1));
+  case ios::beg:
+    return __seek_from_begin(__off, __byte_off);
+  case ios::cur:
+    return __seek_from_current(__off, __byte_off);
+  case ios::end:
+    return __seek_from_end(__off, __byte_off);
   }
-  if (__fseek(__file_, __width > 0 ? __width * __off : 0, __whence))
-    return pos_type(off_type(-1));
-  pos_type __r = __ftell(__file_);
-  __r.state(__st_);
-  return __r;
+  std::__libcpp_unreachable();
 }
 
 template <class _CharT, class _Traits>
@@ -1097,7 +1173,7 @@ int basic_filebuf<_CharT, _Traits>::__fseek(FILE* __file, pos_type __offset, int
 }
 
 template <class _CharT, class _Traits>
-typename basic_filebuf<_CharT, _Traits>::pos_type basic_filebuf<_CharT, _Traits>::__ftell(FILE* __file) {
+typename basic_filebuf<_CharT, _Traits>::off_type basic_filebuf<_CharT, _Traits>::__ftell(FILE* __file) {
 #    ifdef _WIN32
   return _ftelli64(__file);
 #    elif _LIBCPP_LIBC_NEWLIB
diff --git a/libcxx/test/benchmarks/streams/fstream.bench.cpp b/libcxx/test/benchmarks/streams/fstream.bench.cpp
index b9001bd7194573..a1b55cf366aa60 100644
--- a/libcxx/test/benchmarks/streams/fstream.bench.cpp
+++ b/libcxx/test/benchmarks/streams/fstream.bench.cpp
@@ -6,8 +6,9 @@
 //
 //===----------------------------------------------------------------------===//
 
-// UNSUPPORTED: c++03
+// UNSUPPORTED: c++03, c++11, c++14
 
+#include <cassert>
 #include <fstream>
 #include <vector>
 
@@ -29,8 +30,10 @@ static TEST_ALIGN_BENCHMARK void bm_ifstream_read(benchmark::State& state) {
   std::vector<char> buffer;
   buffer.resize(16384);
 
-  std::ofstream gen_testfile("testfile");
-  gen_testfile.write(buffer.data(), buffer.size());
+  {
+    std::ofstream gen_testfile("testfile");
+    gen_testfile.write(buffer.data(), buffer.size());
+  }
 
   std::ifstream stream("testfile");
   assert(stream);
@@ -43,4 +46,56 @@ static TEST_ALIGN_BENCHMARK void bm_ifstream_read(benchmark::State& state) {
 }
 BENCHMARK(bm_ifstream_read)->Name("std::ifstream::read(char*, size)");
 
+void run_sizes(benchmark::Benchmark* benchmark) { benchmark->Arg(0)->Arg(100)->Arg(4000)->Arg(10000); }
+
+template <bool prime>
+static void bm_seekoff_cur(benchmark::State& state) {
+  std::vector<char> buffer;
+  buffer.resize(16384);
+
+  {
+    std::ofstream gen_testfile("testfile");
+    gen_testfile.write(buffer.data(), buffer.size());
+  }
+
+  std::ifstream stream("testfile");
+  assert(stream);
+
+  auto val = state.range();
+
+  for (auto _ : state) {
+    if constexpr (prime)
+      benchmark::DoNotOptimize(stream.rdbuf()->sgetc());
+    benchmark::DoNotOptimize(stream.seekg(val, std::ios::cur));
+    val = -val;
+  }
+}
+BENCHMARK(bm_seekoff_cur<true>)->Name("std::ifstream::seekg(N, std::ios::cur) (primed buffer)")->Apply(run_sizes);
+BENCHMARK(bm_seekoff_cur<false>)->Name("std::ifstream::seekg(N, std::ios::cur) (unprimed buffer)")->Apply(run_sizes);
+
+template <bool prime>
+static void bm_seekoff_beg(benchmark::State& state) {
+  std::vector<char> buffer;
+  buffer.resize(16384);
+
+  {
+    std::ofstream gen_testfile("testfile");
+    gen_testfile.write(buffer.data(), buffer.size());
+  }
+
+  std::ifstream stream("testfile");
+  assert(stream);
+
+  auto val = state.range();
+
+  for (auto _ : state) {
+    if constexpr (prime)
+      benchmark::DoNotOptimize(stream.rdbuf()->sgetc());
+    benchmark::DoNotOptimize(stream.seekg(val, std::ios::beg));
+    benchmark::DoNotOptimize(stream.seekg(0, std::ios::beg));
+  }
+}
+BENCHMARK(bm_seekoff_beg<true>)->Name("std::ifstream::seekg(N, std::ios::beg) (primed buffer)")->Apply(run_sizes);
+BENCHMARK(bm_seekoff_beg<false>)->Name("std::ifstream::seekg(N, std::ios::beg) (unprimed buffer)")->Apply(run_sizes);
+
 BENCHMARK_MAIN();
diff --git a/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.dat b/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.dat
new file mode 100644
index 00000000000000..6a537b5b367880
--- /dev/null
+++ b/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.dat
@@ -0,0 +1 @@
+1234567890
\ No newline at end of file
diff --git a/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.pass.cpp b/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.pass.cpp
index f6378e7998ee98..652fcda725e4f9 100644
--- a/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.pass.cpp
+++ b/libcxx/test/std/input.output/file.streams/fstreams/filebuf.virtuals/seekoff.pass.cpp
@@ -6,6 +6,8 @@
 //
 //===----------------------------------------------------------------------===//
 
+// FILE_DEPENDENCIES: seekoff.dat
+
 // <fstream>
 
 // pos_type seekoff(off_type off, ios_base::seekdir way,
@@ -13,57 +15,399 @@
 // pos_type seekpos(pos_type sp,
 //                  ios_base::openmode which = ios_base::in | ios_base::out);
 
-#include <fstream>
 #include <cassert>
+#include <fstream>
 
+#include "locale_facets.h"
+#include "make_string.h"
 #include "test_macros.h"
 
-int main(int, char**)
-{
-    {
-        char buf[10];
-        typedef std::filebuf::pos_type pos_type;
-        std::filebuf f;
-        f.pubsetbuf(buf, sizeof(buf));
-        assert(f.open("seekoff.dat", std::ios_base::in | std::ios_base::out
-                                                       | std::ios_base::trunc) != 0);
-        assert(f.is_open());
-        f.sputn("abcdefghijklmnopqrstuvwxyz", 26);
-        pos_type p = f.pubseekoff(-15, std::ios_base::cur);
-        assert(p == 11);
-        assert(f.sgetc() == 'l');
-        f.pubseekoff(0, std::ios_base::beg);
-        assert(f.sgetc() == 'a');
-        f.pubseekoff(-1, std::ios_base::end);
-        assert(f.sgetc() == 'z');
-        assert(f.pubseekpos(p) == p);
-        assert(f.sgetc() == 'l');
-    }
-    std::remove("seekoff.dat");
+template <class CharT>
+void reopen(std::basic_filebuf<CharT>& fb) {
+  fb.close();
+  fb.open("seekoff.dat", std::ios::in);
+}
+
+template <class CharT>
+void test() {
+  using filebuf = std::basic_filebuf<CharT>;
+
+  { // seek with file closed
+    filebuf fb;
+    assert(fb.pubseekoff(0, std::ios::beg) == -1);
+  }
+
+  {   // seek stream from begin
+    { // unbuffered
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::beg) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.sgetc() == '1');
+
+      reopen(fb);
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.sgetc() == '2');
+
+      reopen(fb);
+      assert(fb.pubseekoff(10, std::ios::beg) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // buffered
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+
+      assert(fb.sgetc() == '1'); // prime the buffer
+
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::beg) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.sgetc() == CharT('1'));
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.sgetc() == CharT('2'));
+      assert(fb.pubseekoff(10, std::ios::beg) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // buffered, tiny buffer
+      filebuf fb;
+      CharT buffer[5];
+      fb.pubsetbuf(buffer, 5);
+      assert(fb.open("seekoff.dat", std::ios::in));
+      assert(fb.sgetc() == '1');
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::beg) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.sgetc() == CharT('1'));
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.sgetc() == CharT('2'));
+      assert(fb.pubseekoff(8, std::ios::beg) == 8);
+      assert(fb.sgetc() == CharT('9'));
+      assert(fb.pubseekoff(10, std::ios::beg) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // input/output stream, neither read from nor written to
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out));
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::beg) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.pubseekoff(10, std::ios::beg) == 10);
+    }
+    { // input/output stream, written to
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out | std::ios::trunc));
+      fb.sputn(MAKE_CSTRING(CharT, "1234567890"), 10);
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::beg) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.pubseekoff(10, std::ios::beg) == 10);
+    }
+    { // input/output stream, read from
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out));
+
+      assert(fb.pubseekoff(0, std::ios::beg) == 0); // Go to the start, so we can read something
+
+      CharT buffer[11];
+      buffer[10] = '\0';
+      assert(fb.sgetn(buffer, 10) == 10);
+      assert(buffer == MAKE_STRING(CharT, "1234567890"));
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::beg) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.pubseekoff(10, std::ios::beg) == 10);
+    }
+  }
+  {   // seek stream from current position
+    { // unbuffered
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+      // negative offsets, from begin
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+      assert(fb.pubseekoff(-2, std::ios::cur) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 0);
+      assert(fb.pubseekoff(1, std::ios::cur) == 1);
+
+      // negative offset, from 1 character into the stream
+      assert(fb.pubseekoff(-1, std::ios::cur) == 0);
+
+      assert(fb.pubseekoff(10, std::ios::cur) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // buffered
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+      assert(fb.sgetc() == '1');
+
+      // negative offsets, from begin
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+      assert(fb.pubseekoff(-2, std::ios::cur) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 0);
+      assert(fb.pubseekoff(1, std::ios::cur) == 1);
+
+      // negative offset, from 1 character into the stream
+      assert(fb.pubseekoff(-1, std::ios::cur) == 0);
+
+      assert(fb.pubseekoff(10, std::ios::cur) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // buffered, tiny buffer
+      filebuf fb;
+      CharT buffer[5];
+      fb.pubsetbuf(buffer, 5);
+      assert(fb.open("seekoff.dat", std::ios::in));
+      assert(fb.sgetc() == '1');
+
+      // negative offsets, from begin
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+      assert(fb.pubseekoff(-2, std::ios::cur) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 0);
+      assert(fb.pubseekoff(1, std::ios::cur) == 1);
+
+      // negative offset, from 1 character into the stream
+      assert(fb.pubseekoff(-1, std::ios::cur) == 0);
+
+      assert(fb.pubseekoff(10, std::ios::cur) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // input/output stream, neither read from nor written to
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out));
+
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+      assert(fb.pubseekoff(-2, std::ios::cur) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 0);
+      assert(fb.pubseekoff(1, std::ios::cur) == 1);
+
+      // negative offset, from 1 character into the stream
+      assert(fb.pubseekoff(-1, std::ios::cur) == 0);
+      assert(fb.pubseekoff(10, std::ios::cur) == 10);
+    }
+    { // input/output stream, written to
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out | std::ios::trunc));
+      fb.sputn(MAKE_CSTRING(CharT, "1234567890"), 10);
+
+      // negative offsets; we've written and the cursor is at the end of the file, so these succeed
+      assert(fb.pubseekoff(-1, std::ios::cur) == 9);
+      assert(fb.pubseekoff(-2, std::ios::cur) == 7);
+
+      // except this one, we didn't write this much
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 7);
+      assert(fb.pubseekoff(1, std::ios::cur) == 8);
+      assert(fb.pubseekoff(10, std::ios::cur) == 18);
+    }
+    { // input/output stream, read from
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out));
+
+      assert(fb.pubseekoff(0, std::ios::beg) == 0); // Go to the start, so we can read something
+
+      CharT buffer[11];
+      buffer[10] = '\0';
+      assert(fb.sgetn(buffer, 10) == 10);
+      assert(buffer == MAKE_STRING(CharT, "1234567890"));
+
+      // negative offsets; we've read and the cursor is at the end of the file, so these succeed
+      assert(fb.pubseekoff(-1, std::ios::cur) == 9);
+      assert(fb.pubseekoff(-2, std::ios::cur) == 7);
+
+      // except this one, we didn't read this much
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 7);
+      assert(fb.pubseekoff(1, std::ios::cur) == 8);
+      assert(fb.pubseekoff(10, std::ios::cur) == 18);
+    }
+  }
+  {   // seek stream end
+    { // unbuffered
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // buffered
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+      assert(fb.sgetc() == '1');
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // buffered, tiny buffer
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in));
+      CharT buffer[5];
+      fb.pubsetbuf(buffer, 5);
+      assert(fb.sgetc() == '1');
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // input/output stream, neither read from nor written to
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out));
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // input/output stream, written to
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out | std::ios::trunc));
+      fb.sputn(MAKE_CSTRING(CharT, "1234567890"), 10);
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+    { // input/output stream, read from
+      filebuf fb;
+      assert(fb.open("seekoff.dat", std::ios::in | std::ios::out));
+
+      assert(fb.pubseekoff(0, std::ios::beg) == 0); // Go to the start, so we can read something
+
+      CharT buffer[11];
+      buffer[10] = '\0';
+      assert(fb.sgetn(buffer, 10) == 10);
+      assert(buffer == MAKE_STRING(CharT, "1234567890"));
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == std::char_traits<CharT>::eof());
+    }
+  }
+}
+
+void test_different_codecvt_facets() {
+  using filebuf = std::basic_filebuf<char, facet::char_traits>;
+  {   // converting, constant size facet
+    { // seek stream from begin
+      filebuf fb;
+      fb.pubimbue(facet::get_codecvt_locale<facet::codecvt_constant_converting>());
+      fb.open("seekoff.dat", std::ios::in);
+
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.sgetc() == '1' - 30);
+
+      // posititve offset
+      assert(fb.pubseekoff(1, std::ios::beg) == 1);
+      assert(fb.sgetc() == '2' - 30);
+    }
+    { // seek from current position
+      filebuf fb;
+      fb.pubimbue(facet::get_codecvt_locale<facet::codecvt_constant_converting>());
+      fb.open("seekoff.dat", std::ios::in);
+      // negative offsets, from begin
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+      assert(fb.pubseekoff(-2, std::ios::cur) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 0);
+      assert(fb.pubseekoff(1, std::ios::cur) == 1);
+
+      // negative offset, from 1 character into the stream
+      assert(fb.pubseekoff(-1, std::ios::cur) == 0);
+
+      assert(fb.pubseekoff(10, std::ios::cur) == 10);
+      assert(fb.sgetc() == facet::char_traits::eof());
+    }
+    { // seek from end
+      filebuf fb;
+      fb.pubimbue(facet::get_codecvt_locale<facet::codecvt_constant_converting>());
+      fb.open("seekoff.dat", std::ios::in);
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == facet::char_traits::eof());
+    }
+  }
+  {   // converting, variable size facet
+    { // seek stream from begin
+      filebuf fb;
+      fb.pubimbue(facet::get_codecvt_locale<facet::codecvt_variable_converting>());
+      fb.open("seekoff.dat", std::ios::in);
+
+      // negative offsets
+      assert(fb.pubseekoff(-1, std::ios::beg) == -1);
+      assert(fb.pubseekoff(-2, std::ios::beg) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::beg) == 0);
+      assert(fb.sgetc() == '1');
+
+      // posititve offset
+      assert(fb.pubseekoff(1, std::ios::beg) == -1);
+    }
+    { // seek from current position
+      filebuf fb;
+      fb.pubimbue(facet::get_codecvt_locale<facet::codecvt_variable_converting>());
+      fb.open("seekoff.dat", std::ios::in);
+      // negative offsets, from begin
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+      assert(fb.pubseekoff(-2, std::ios::cur) == -1);
+      assert(fb.pubseekoff(std::numeric_limits<typename filebuf::off_type>::min(), std::ios::cur) == -1);
+
+      // zero offset
+      assert(fb.pubseekoff(0, std::ios::cur) == 0);
+      assert(fb.pubseekoff(1, std::ios::cur) == -1);
+
+      assert(fb.sbumpc() == '1');
+      // negative offset, from 1 character into the stream
+      assert(fb.pubseekoff(-1, std::ios::cur) == -1);
+    }
+    { // seek from end
+      filebuf fb;
+      fb.pubimbue(facet::get_codecvt_locale<facet::codecvt_variable_converting>());
+      fb.open("seekoff.dat", std::ios::in);
+      assert(fb.pubseekoff(0, std::ios::end) == 10);
+      assert(fb.sgetc() == facet::char_traits::eof());
+      assert(fb.pubseekoff(-1, std::ios::end) == -1);
+    }
+  }
+}
 
+int main(int, char**) {
+  test<char>();
 #ifndef TEST_HAS_NO_WIDE_CHARACTERS
-    {
-        wchar_t buf[10];
-        typedef std::filebuf::pos_type pos_type;
-        std::wfilebuf f;
-        f.pubsetbuf(buf, sizeof(buf)/sizeof(buf[0]));
-        assert(f.open("seekoff.dat", std::ios_base::in | std::ios_base::out
-                                                       | std::ios_base::trunc) != 0);
-        assert(f.is_open());
-        f.sputn(L"abcdefghijklmnopqrstuvwxyz", 26);
-        LIBCPP_ASSERT(buf[0] == L'v');
-        pos_type p = f.pubseekoff(-15, std::ios_base::cur);
-        assert(p == 11);
-        assert(f.sgetc() == L'l');
-        f.pubseekoff(0, std::ios_base::beg);
-        assert(f.sgetc() == L'a');
-        f.pubseekoff(-1, std::ios_base::end);
-        assert(f.sgetc() == L'z');
-        assert(f.pubseekpos(p) == p);
-        assert(f.sgetc() == L'l');
-    }
-    std::remove("seekoff.dat");
+  test<wchar_t>();
 #endif
+  test_different_codecvt_facets();
 
   return 0;
 }
diff --git a/libcxx/test/support/locale_facets.h b/libcxx/test/support/locale_facets.h
new file mode 100644
index 00000000000000..a83c6519165614
--- /dev/null
+++ b/libcxx/test/support/locale_facets.h
@@ -0,0 +1,275 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 LIBCXX_TEST_SUPPORT_LOCALE_FACETS_H
+#define LIBCXX_TEST_SUPPORT_LOCALE_FACETS_H
+
+#include <algorithm>
+#include <cstring>
+#include <locale>
+
+namespace facet {
+struct state_t {
+  char next_char;
+  bool has_next_char;
+  bool expect_at_least_5;
+};
+
+struct char_traits {
+  using base = std::char_traits<char>;
+
+  using char_type  = base::char_type;
+  using int_type   = base::int_type;
+  using off_type   = base::off_type;
+  using pos_type   = std::fpos<state_t>;
+  using state_type = state_t;
+
+  static void assign(char_type& lhs, char_type& rhs) { lhs = rhs; }
+  static bool eq(char_type lhs, char_type rhs) { return lhs == rhs; }
+  static bool lt(char_type lhs, char_type rhs) {
+    return static_cast<unsigned char>(lhs) < static_cast<unsigned char>(rhs);
+  }
+
+  static int compare(const char_type* lhs, const char_type* rhs, size_t n) { return std::memcmp(lhs, rhs, n); }
+
+  static size_t length(const char_type* str) { return std::strlen(str); }
+
+  static const char_type* find(const char_type* str, size_t count, const char_type& c) {
+    return static_cast<const char_type*>(std::memchr(str, c, count));
+  }
+
+  static char_type* move(char_type* dst, const char_type* src, size_t count) {
+    return static_cast<char_type*>(std::memmove(dst, src, count));
+  }
+
+  static char_type* copy(char_type* dst, const char_type* src, size_t count) {
+    return static_cast<char_type*>(std::memcpy(dst, src, count));
+  }
+
+  static char_type* assign(char_type* dst, size_t count, char_type c) {
+    return static_cast<char_type*>(std::memset(dst, c, count));
+  }
+
+  static int_type not_eof(int_type c) { return eq_int_type(c, eof()) ? ~eof() : c; }
+  static char_type to_char_type(int_type c) { return c; }
+  static int_type to_int_type(char_type c) { return c; }
+  static bool eq_int_type(int_type lhs, int_type rhs) { return lhs == rhs; }
+  static int_type eof() { return EOF; }
+};
+} // namespace facet
+
+template <>
+class std::codecvt<char, char, facet::state_t> : public std::locale::facet, public std::codecvt_base {
+public:
+  using intern_type = char;
+  using extern_type = char;
+  using state_type  = ::facet::state_t;
+
+  static locale::id id;
+
+  explicit codecvt(size_t refs = 0) : locale::facet(refs) {}
+
+  result out(state_type& state,
+             const intern_type* from_first,
+             const intern_type* from_last,
+             const intern_type*& from_next,
+             extern_type* to_first,
+             extern_type* to_last,
+             extern_type*& to_next) const {
+    return do_out(state, from_first, from_last, from_next, to_first, to_last, to_next);
+  }
+
+  result unshift(state_type& state, extern_type* to_first, extern_type* to_last, extern_type*& to_next) const {
+    return do_unshift(state, to_first, to_last, to_next);
+  }
+
+  result in(state_type& state,
+            const extern_type* from_first,
+            const extern_type* from_last,
+            const extern_type*& from_next,
+            intern_type* to_first,
+            intern_type* to_last,
+            intern_type*& to_next) const {
+    return do_in(state, from_first, from_last, from_next, to_first, to_last, to_next);
+  }
+
+  int encoding() const { return do_encoding(); }
+  bool always_noconv() const { return do_always_noconv(); }
+  int length(state_type& state, const extern_type* from_first, const extern_type* from_last, size_t max) const {
+    return do_length(state, from_first, from_last, max);
+  }
+
+  int max_length() const { return do_max_length(); }
+
+protected:
+  virtual result
+  do_out(state_type& state,
+         const intern_type* from_first,
+         const intern_type* from_last,
+         const intern_type*& from_next,
+         extern_type* to_first,
+         extern_type* to_last,
+         extern_type*& to_next) const = 0;
+
+  virtual result
+  do_unshift(state_type& state, extern_type* to_first, extern_type* to_last, extern_type*& to_next) const = 0;
+
+  virtual result
+  do_in(state_type& state,
+        const extern_type* from_first,
+        const extern_type* from_last,
+        const extern_type*& from_next,
+        intern_type* to_first,
+        intern_type* to_last,
+        intern_type*& to_next) const = 0;
+
+  virtual int do_encoding() const       = 0;
+  virtual bool do_always_noconv() const = 0;
+  virtual int
+  do_length(state_type& state, const extern_type* from_first, const extern_type* from_last, size_t max) const = 0;
+  virtual int do_max_length() const                                                                           = 0;
+};
+
+std::locale::id std::codecvt<char, char, facet::state_t>::id;
+
+namespace facet {
+
+using codecvt_base_t = std::codecvt<char, char, facet::state_t>;
+
+class codecvt_constant_converting final : public codecvt_base_t {
+public:
+  template <bool ToExtern>
+  static result do_conversion(
+      const char* from_first,
+      const char* from_last,
+      const char*& from_next,
+      char* to_first,
+      char* to_last,
+      char*& to_next) {
+    auto size = std::min(from_last - from_first, to_last - to_first);
+    std::transform(from_first, from_first + size, to_first, [](unsigned char c) -> char {
+      return ToExtern ? c + 30 : c - 30;
+    });
+    from_next = from_first + size;
+    to_next   = to_first + size;
+    return size == from_last - from_first ? ok : partial;
+  }
+
+  result do_out(state_type&,
+                const intern_type* from_first,
+                const intern_type* from_last,
+                const intern_type*& from_next,
+                extern_type* to_first,
+                extern_type* to_last,
+                extern_type*& to_next) const override {
+    return do_conversion<true>(from_first, from_last, from_next, to_first, to_last, to_next);
+  }
+
+  result
+  do_unshift(state_type&, extern_type* to_first, extern_type* /*to_last*/, extern_type*& to_next) const override {
+    to_next = to_first;
+    return noconv;
+  }
+
+  result do_in(state_type&,
+               const extern_type* from_first,
+               const extern_type* from_last,
+               const extern_type*& from_next,
+               intern_type* to_first,
+               intern_type* to_last,
+               intern_type*& to_next) const override {
+    return do_conversion<false>(from_first, from_last, from_next, to_first, to_last, to_next);
+  }
+
+  int do_encoding() const override { return 1; }
+  bool do_always_noconv() const override { return false; }
+  int do_length(state_type&, const extern_type* from_first, const extern_type* from_last, size_t max) const override {
+    return static_cast<int>(std::min(max, static_cast<size_t>(from_last - from_first)));
+  }
+  int do_max_length() const override { return 1; }
+};
+
+class codecvt_variable_converting final : public codecvt_base_t {
+  result do_out(state_type& state,
+                const intern_type* from_first,
+                const intern_type* from_last,
+                const intern_type*& from_next,
+                extern_type* to_first,
+                extern_type* to_last,
+                extern_type*& to_next) const override {
+    for (; from_first != from_last && to_first != to_last; ++from_first, ++to_first) {
+      if (*from_first == -1) {
+        state.expect_at_least_5 = true;
+        continue;
+      }
+      char c = *from_first;
+      if (state.expect_at_least_5 ? c < '5' : c >= '5') {
+        from_next = from_first;
+        to_next   = to_first;
+        return error;
+      }
+      *to_first               = c;
+      state.expect_at_least_5 = false;
+    }
+    return from_first == from_last ? ok : partial;
+  }
+
+  result
+  do_unshift(state_type& state, extern_type* to_first, extern_type* /*to_last*/, extern_type*& to_next) const override {
+    if (state.expect_at_least_5)
+      return error;
+    to_next = to_first;
+    return noconv;
+  }
+
+  result do_in(state_type& state,
+               const extern_type* from_first,
+               const extern_type* from_last,
+               const extern_type*& from_next,
+               intern_type* to_first,
+               intern_type* to_last,
+               intern_type*& to_next) const override {
+    if (state.has_next_char) {
+      if (to_first != to_last) {
+        *to_first++         = state.next_char;
+        state.has_next_char = false;
+      }
+    }
+    for (; from_first != from_last && to_first != to_last; ++from_first, ++to_first) {
+      if (*from_first >= '5') {
+        if (to_first + 1 == to_last) {
+          state.has_next_char = true;
+          state.next_char     = *from_first;
+          *to_first           = -1;
+          break;
+        } else {
+          *to_first++ = -1;
+        }
+      }
+      *to_first = *from_first;
+    }
+    from_next = from_first;
+    to_next   = to_first;
+    return from_first == from_last && !state.has_next_char ? ok : partial;
+  }
+
+  int do_encoding() const override { return 0; }
+  bool do_always_noconv() const override { return false; }
+  int do_length(state_type&, const extern_type* from_first, const extern_type* from_last, size_t max) const override {
+    return static_cast<int>(std::min(max, static_cast<size_t>(from_last - from_first)));
+  }
+  int do_max_length() const override { return 2; }
+};
+
+template <class CodeCVT>
+inline std::locale get_codecvt_locale() {
+  return std::locale(std::locale::classic(), new CodeCVT);
+}
+} // namespace facet
+
+#endif // LIBCXX_TEST_SUPPORT_LOCALE_FACETS_H
diff --git a/libcxx/test/support/module.modulemap b/libcxx/test/support/module.modulemap
index 0d83f08b6dd225..ec980789f48c23 100644
--- a/libcxx/test/support/module.modulemap
+++ b/libcxx/test/support/module.modulemap
@@ -5,6 +5,7 @@ module test_config {
 
 module test {
   module double_move_tracker    { header "double_move_tracker.h" }
+  module locale_facets          { header "locale_facets.h" }
   module test_allocator         { header "test_allocator.h" }
   module test_iterators         { header "test_iterators.h" }
   module test_text_encoding     { header "test_text_encoding.h" }



More information about the libcxx-commits mailing list