100.00% Lines (13/13) 100.00% Functions (6/6)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2026 Michael Vandeberg 2   // Copyright (c) 2026 Michael Vandeberg
3   // 3   //
4   // Distributed under the Boost Software License, Version 1.0. (See accompanying 4   // Distributed under the Boost Software License, Version 1.0. (See accompanying
5   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 5   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6   // 6   //
7   // Official repository: https://github.com/cppalliance/corosio 7   // Official repository: https://github.com/cppalliance/corosio
8   // 8   //
9   9  
10   #ifndef BOOST_COROSIO_STREAM_FILE_HPP 10   #ifndef BOOST_COROSIO_STREAM_FILE_HPP
11   #define BOOST_COROSIO_STREAM_FILE_HPP 11   #define BOOST_COROSIO_STREAM_FILE_HPP
12   12  
13   #include <boost/corosio/detail/config.hpp> 13   #include <boost/corosio/detail/config.hpp>
14   #include <boost/corosio/detail/platform.hpp> 14   #include <boost/corosio/detail/platform.hpp>
15   #include <boost/corosio/detail/native_handle.hpp> 15   #include <boost/corosio/detail/native_handle.hpp>
16   #include <boost/corosio/error.hpp> 16   #include <boost/corosio/error.hpp>
17   #include <boost/corosio/file_base.hpp> 17   #include <boost/corosio/file_base.hpp>
18   #include <boost/corosio/io/io_stream.hpp> 18   #include <boost/corosio/io/io_stream.hpp>
19   #include <boost/capy/ex/execution_context.hpp> 19   #include <boost/capy/ex/execution_context.hpp>
20   #include <boost/capy/concept/executor.hpp> 20   #include <boost/capy/concept/executor.hpp>
21   #include <boost/capy/results.hpp> 21   #include <boost/capy/results.hpp>
22   22  
23   #include <concepts> 23   #include <concepts>
24   #include <cstdint> 24   #include <cstdint>
25   #include <filesystem> 25   #include <filesystem>
26   #include <system_error> 26   #include <system_error>
27   27  
28   namespace boost::corosio { 28   namespace boost::corosio {
29   29  
30   /** Reads and writes a file sequentially, from a coroutine. 30   /** Reads and writes a file sequentially, from a coroutine.
31   31  
32   Provides asynchronous read and write operations on a regular 32   Provides asynchronous read and write operations on a regular
33   file with an implicit position that advances after each 33   file with an implicit position that advances after each
34   operation. 34   operation.
35   35  
36   Inherits from @ref io_stream, so `read_some` and `write_some` 36   Inherits from @ref io_stream, so `read_some` and `write_some`
37   are available and work with any algorithm that accepts an 37   are available and work with any algorithm that accepts an
38   `io_stream&`. 38   `io_stream&`.
39   39  
40   On POSIX platforms, file I/O is dispatched to a thread pool 40   On POSIX platforms, file I/O is dispatched to a thread pool
41   (blocking `preadv`/`pwritev`) with completion posted back to 41   (blocking `preadv`/`pwritev`) with completion posted back to
42   the scheduler. On Windows, true overlapped I/O is used via IOCP. 42   the scheduler. On Windows, true overlapped I/O is used via IOCP.
43   43  
44   On Windows, while the file is open, its handle is bound to the 44   On Windows, while the file is open, its handle is bound to the
45   execution context's completion port. Every overlapped call on the 45   execution context's completion port. Every overlapped call on the
46   handle queues a packet to that port. Do not issue your own 46   handle queues a packet to that port. Do not issue your own
47   overlapped I/O on `native_handle()` (`DeviceIoControl`, 47   overlapped I/O on `native_handle()` (`DeviceIoControl`,
48   `ReadFile`) unless the `OVERLAPPED`'s `hEvent` has its low-order 48   `ReadFile`) unless the `OVERLAPPED`'s `hEvent` has its low-order
49   bit set, which suppresses the packet. 49   bit set, which suppresses the packet.
50   50  
51   @par Thread Safety 51   @par Thread Safety
52   Distinct objects: Safe.@n 52   Distinct objects: Safe.@n
53   Shared objects: Unsafe. Only one asynchronous operation 53   Shared objects: Unsafe. Only one asynchronous operation
54   may be in flight at a time. 54   may be in flight at a time.
55   55  
56   @par Example 56   @par Example
57   @par !example stream_file 57   @par !example stream_file
58   */ 58   */
59   class BOOST_COROSIO_DECL stream_file : public io_stream 59   class BOOST_COROSIO_DECL stream_file : public io_stream
60   { 60   {
61   public: 61   public:
62   /** Defines the file operations a platform backend implements. 62   /** Defines the file operations a platform backend implements.
63   63  
64   Backends derive from this to provide file I/O. 64   Backends derive from this to provide file I/O.
65   `read_some` and `write_some` are inherited from 65   `read_some` and `write_some` are inherited from
66   @ref io_stream::implementation. 66   @ref io_stream::implementation.
67   */ 67   */
68   struct implementation : io_stream::implementation 68   struct implementation : io_stream::implementation
69   { 69   {
70   /// Return the platform file descriptor or handle. 70   /// Return the platform file descriptor or handle.
71   virtual native_handle_type native_handle() const noexcept = 0; 71   virtual native_handle_type native_handle() const noexcept = 0;
72   72  
73   /// Cancel pending asynchronous operations. 73   /// Cancel pending asynchronous operations.
74   virtual void cancel() noexcept = 0; 74   virtual void cancel() noexcept = 0;
75   75  
76   /** Return the file size in bytes. 76   /** Return the file size in bytes.
77   77  
78   @return The error code and the current size, in bytes. 78   @return The error code and the current size, in bytes.
79   */ 79   */
80   virtual capy::results<std::uint64_t> size() const noexcept = 0; 80   virtual capy::results<std::uint64_t> size() const noexcept = 0;
81   81  
82   /** Resize the file to @p new_size bytes. 82   /** Resize the file to @p new_size bytes.
83   83  
84   @param new_size The requested size in bytes. 84   @param new_size The requested size in bytes.
85   85  
86   @return The error code, empty on success. 86   @return The error code, empty on success.
87   */ 87   */
88   virtual std::error_code resize(std::uint64_t new_size) noexcept = 0; 88   virtual std::error_code resize(std::uint64_t new_size) noexcept = 0;
89   89  
90   /** Synchronize file data to stable storage. 90   /** Synchronize file data to stable storage.
91   91  
92   @return The error code, empty on success. 92   @return The error code, empty on success.
93   */ 93   */
94   virtual std::error_code sync_data() noexcept = 0; 94   virtual std::error_code sync_data() noexcept = 0;
95   95  
96   /** Synchronize file data and metadata to stable storage. 96   /** Synchronize file data and metadata to stable storage.
97   97  
98   @return The error code, empty on success. 98   @return The error code, empty on success.
99   */ 99   */
100   virtual std::error_code sync_all() noexcept = 0; 100   virtual std::error_code sync_all() noexcept = 0;
101   101  
102   /** Release ownership of the native handle. 102   /** Release ownership of the native handle.
103   103  
104   On success the caller owns the returned handle. 104   On success the caller owns the returned handle.
105   105  
106   @return The error code and the native handle. 106   @return The error code and the native handle.
107   */ 107   */
108   virtual capy::results<native_handle_type> release() noexcept = 0; 108   virtual capy::results<native_handle_type> release() noexcept = 0;
109   109  
110   /** Adopt an existing native handle. 110   /** Adopt an existing native handle.
111   111  
112   @param handle The native handle to adopt. The implementation takes 112   @param handle The native handle to adopt. The implementation takes
113   ownership and closes it. 113   ownership and closes it.
114   114  
115   @return The error code, empty on success. 115   @return The error code, empty on success.
116   */ 116   */
117   virtual std::error_code assign(native_handle_type handle) noexcept = 0; 117   virtual std::error_code assign(native_handle_type handle) noexcept = 0;
118   118  
119   /** Move the file position. 119   /** Move the file position.
120   120  
121   @param offset Signed offset from @p origin. 121   @param offset Signed offset from @p origin.
122   @param origin The reference point for the seek. 122   @param origin The reference point for the seek.
123   @return The error code and new absolute position. 123   @return The error code and new absolute position.
124   */ 124   */
125   virtual capy::results<std::uint64_t> 125   virtual capy::results<std::uint64_t>
126   seek(std::int64_t offset, file_base::seek_basis origin) noexcept = 0; 126   seek(std::int64_t offset, file_base::seek_basis origin) noexcept = 0;
127   }; 127   };
128   128  
129   /** Closes the file if open, cancelling any pending operations. 129   /** Closes the file if open, cancelling any pending operations.
130   */ 130   */
131   ~stream_file() override; 131   ~stream_file() override;
132   132  
133   /** Construct from an execution context. 133   /** Construct from an execution context.
134   134  
135   @param ctx The execution context that owns this file. 135   @param ctx The execution context that owns this file.
136   */ 136   */
137   explicit stream_file(capy::execution_context& ctx); 137   explicit stream_file(capy::execution_context& ctx);
138   138  
139   /** Construct from an executor. 139   /** Construct from an executor.
140   140  
141   @param ex The executor whose context owns this file. 141   @param ex The executor whose context owns this file.
142   */ 142   */
143   template<class Ex> 143   template<class Ex>
144   requires(!std::same_as<std::remove_cvref_t<Ex>, stream_file>) && 144   requires(!std::same_as<std::remove_cvref_t<Ex>, stream_file>) &&
145   capy::Executor<Ex> 145   capy::Executor<Ex>
HITCBC 146   2 explicit stream_file(Ex const& ex) : stream_file(ex.context()) 146   2 explicit stream_file(Ex const& ex) : stream_file(ex.context())
147   { 147   {
HITCBC 148   2 } 148   2 }
149   149  
150   /** Transfers ownership of the file resources. 150   /** Transfers ownership of the file resources.
151   */ 151   */
HITCBC 152   6 stream_file(stream_file&& other) noexcept : io_object(std::move(other)) {} 152   6 stream_file(stream_file&& other) noexcept : io_object(std::move(other)) {}
153   153  
154   /** Closes any existing file and transfers ownership. 154   /** Closes any existing file and transfers ownership.
155   155  
156   @return Reference to this object. 156   @return Reference to this object.
157   */ 157   */
HITCBC 158   2 stream_file& operator=(stream_file&& other) noexcept 158   2 stream_file& operator=(stream_file&& other) noexcept
159   { 159   {
HITCBC 160   2 if (this != &other) 160   2 if (this != &other)
161   { 161   {
HITCBC 162   2 close(); 162   2 close();
HITCBC 163   2 h_ = std::move(other.h_); 163   2 h_ = std::move(other.h_);
164   } 164   }
HITCBC 165   2 return *this; 165   2 return *this;
166   } 166   }
167   167  
168   /// Copy construction is disabled; the handle is uniquely owned. 168   /// Copy construction is disabled; the handle is uniquely owned.
169   stream_file(stream_file const&) = delete; 169   stream_file(stream_file const&) = delete;
170   /// Copy assignment is disabled; the handle is uniquely owned. 170   /// Copy assignment is disabled; the handle is uniquely owned.
171   stream_file& operator=(stream_file const&) = delete; 171   stream_file& operator=(stream_file const&) = delete;
172   172  
173   // read_some() inherited from io_read_stream 173   // read_some() inherited from io_read_stream
174   // write_some() inherited from io_write_stream 174   // write_some() inherited from io_write_stream
175   175  
176   /** Open a file. 176   /** Open a file.
177   177  
178   Failures such as a missing file or insufficient permissions 178   Failures such as a missing file or insufficient permissions
179   are expected runtime conditions and are reported through the 179   are expected runtime conditions and are reported through the
180   returned error code. If the file is already open, it is 180   returned error code. If the file is already open, it is
181   closed first. 181   closed first.
182   182  
183   @param path The filesystem path to open. 183   @param path The filesystem path to open.
184   @param mode Bitmask of @ref file_base::flags specifying 184   @param mode Bitmask of @ref file_base::flags specifying
185   access mode and creation behavior. 185   access mode and creation behavior.
186   186  
187   @return The error code, empty on success. 187   @return The error code, empty on success.
188   */ 188   */
189   [[nodiscard]] std::error_code open( 189   [[nodiscard]] std::error_code open(
190   std::filesystem::path const& path, 190   std::filesystem::path const& path,
191   file_base::flags mode = file_base::read_only) noexcept; 191   file_base::flags mode = file_base::read_only) noexcept;
192   192  
193   /** Close the file. 193   /** Close the file.
194   194  
195   Releases file resources. Pending operations complete through the 195   Releases file resources. Pending operations complete through the
196   same path as @ref cancel: one still in flight completes with 196   same path as @ref cancel: one still in flight completes with
197   `errc::operation_canceled`. An operation whose result is already 197   `errc::operation_canceled`. An operation whose result is already
198   decided reports that result. 198   decided reports that result.
199   */ 199   */
200   void close() noexcept; 200   void close() noexcept;
201   201  
202   /** Check if the file is open. 202   /** Check if the file is open.
203   203  
204   @return `true` if the file is open and ready for I/O. 204   @return `true` if the file is open and ready for I/O.
205   */ 205   */
HITCBC 206   3058 bool is_open() const noexcept 206   3058 bool is_open() const noexcept
207   { 207   {
208   #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS) 208   #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
209   return h_ && get().native_handle() != ~native_handle_type(0); 209   return h_ && get().native_handle() != ~native_handle_type(0);
210   #else 210   #else
HITCBC 211   3058 return h_ && get().native_handle() >= 0; 211   3058 return h_ && get().native_handle() >= 0;
212   #endif 212   #endif
213   } 213   }
214   214  
215   /** Cancel pending asynchronous operations. 215   /** Cancel pending asynchronous operations.
216   216  
217   Operations still in flight complete with 217   Operations still in flight complete with
218   `errc::operation_canceled`; an operation whose result is 218   `errc::operation_canceled`; an operation whose result is
219   already decided reports that result. 219   already decided reports that result.
220   */ 220   */
221   void cancel() noexcept; 221   void cancel() noexcept;
222   222  
223   /** Get the native file descriptor or handle. 223   /** Get the native file descriptor or handle.
224   224  
225   @return The native handle, or -1/INVALID_HANDLE_VALUE 225   @return The native handle, or -1/INVALID_HANDLE_VALUE
226   if not open. 226   if not open.
227   */ 227   */
228   native_handle_type native_handle() const noexcept; 228   native_handle_type native_handle() const noexcept;
229   229  
230   /** Return the file size in bytes. 230   /** Return the file size in bytes.
231   231  
232   A closed file reports `errc::bad_file_descriptor`. Other 232   A closed file reports `errc::bad_file_descriptor`. Other
233   failures carry the error code from the operating system. On 233   failures carry the error code from the operating system. On
234   failure the size is zero. 234   failure the size is zero.
235   235  
236   @return The error code and the file size in bytes. 236   @return The error code and the file size in bytes.
237   */ 237   */
238   [[nodiscard]] capy::results<std::uint64_t> size() const noexcept; 238   [[nodiscard]] capy::results<std::uint64_t> size() const noexcept;
239   239  
240   /** Resize the file to @p new_size bytes. 240   /** Resize the file to @p new_size bytes.
241   241  
242   Failures such as insufficient disk space are reported 242   Failures such as insufficient disk space are reported
243   through the returned error code. A closed file reports 243   through the returned error code. A closed file reports
244   `errc::bad_file_descriptor`. 244   `errc::bad_file_descriptor`.
245   245  
246   @param new_size The new file size. 246   @param new_size The new file size.
247   247  
248   @return The error code, empty on success. 248   @return The error code, empty on success.
249   */ 249   */
250   [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept; 250   [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept;
251   251  
252   /** Synchronize file data to stable storage. 252   /** Synchronize file data to stable storage.
253   253  
254   Write-back failures such as device I/O errors surface here 254   Write-back failures such as device I/O errors surface here
255   and are reported through the returned error code. A closed 255   and are reported through the returned error code. A closed
256   file reports `errc::bad_file_descriptor`. 256   file reports `errc::bad_file_descriptor`.
257   257  
258   @return The error code, empty on success. 258   @return The error code, empty on success.
259   */ 259   */
260   [[nodiscard]] std::error_code sync_data() noexcept; 260   [[nodiscard]] std::error_code sync_data() noexcept;
261   261  
262   /** Synchronize file data and metadata to stable storage. 262   /** Synchronize file data and metadata to stable storage.
263   263  
264   Write-back failures such as device I/O errors surface here 264   Write-back failures such as device I/O errors surface here
265   and are reported through the returned error code. A closed 265   and are reported through the returned error code. A closed
266   file reports `errc::bad_file_descriptor`. 266   file reports `errc::bad_file_descriptor`.
267   267  
268   @return The error code, empty on success. 268   @return The error code, empty on success.
269   */ 269   */
270   [[nodiscard]] std::error_code sync_all() noexcept; 270   [[nodiscard]] std::error_code sync_all() noexcept;
271   271  
272   /** Release ownership of the native handle. 272   /** Release ownership of the native handle.
273   273  
274   On success the file object becomes not-open, and the caller 274   On success the file object becomes not-open, and the caller
275   is responsible for closing the returned handle. 275   is responsible for closing the returned handle.
276   276  
277   `release()` cancels pending operations first. It reports 277   `release()` cancels pending operations first. It reports
278   `errc::device_or_resource_busy` if one is still in flight on 278   `errc::device_or_resource_busy` if one is still in flight on
279   Windows. Where file I/O runs on a thread pool, it reports the 279   Windows. Where file I/O runs on a thread pool, it reports the
280   same while a worker thread is already performing one. Call 280   same while a worker thread is already performing one. Call
281   `release()` again once the cancelled operations have completed. On 281   `release()` again once the cancelled operations have completed. On
282   Windows it reports `errc::operation_not_supported` if the 282   Windows it reports `errc::operation_not_supported` if the
283   handle cannot be detached from the execution context's 283   handle cannot be detached from the execution context's
284   completion port. Detaching requires Windows 8.1 or later. 284   completion port. Detaching requires Windows 8.1 or later.
285   285  
286   A closed file reports `errc::bad_file_descriptor`. On 286   A closed file reports `errc::bad_file_descriptor`. On
287   failure the object keeps the handle and the returned handle 287   failure the object keeps the handle and the returned handle
288   is the invalid sentinel ( -1 or `INVALID_HANDLE_VALUE` ), never 288   is the invalid sentinel ( -1 or `INVALID_HANDLE_VALUE` ), never
289   zero. 289   zero.
290   290  
291   @return The error code and the native file descriptor or handle. 291   @return The error code and the native file descriptor or handle.
292   */ 292   */
293   [[nodiscard]] capy::results<native_handle_type> release() noexcept; 293   [[nodiscard]] capy::results<native_handle_type> release() noexcept;
294   294  
295   /** Adopt an existing native handle. 295   /** Adopt an existing native handle.
296   296  
297   The object must be closed. To replace a held file, `close()` 297   The object must be closed. To replace a held file, `close()`
298   or `release()` it first. On success the object takes 298   or `release()` it first. On success the object takes
299   ownership of @p handle. Handles created elsewhere may be 299   ownership of @p handle. Handles created elsewhere may be
300   unsuitable for asynchronous I/O. `assign()` reports most such 300   unsuitable for asynchronous I/O. `assign()` reports most such
301   failures through the returned error code. 301   failures through the returned error code.
302   302  
303   @param handle The native file descriptor or handle. 303   @param handle The native file descriptor or handle.
304   304  
305   @return An error code describing the outcome. 305   @return An error code describing the outcome.
306   `error::already_open` if this object is open. 306   `error::already_open` if this object is open.
307   `errc::bad_file_descriptor` if @p handle is invalid. 307   `errc::bad_file_descriptor` if @p handle is invalid.
308   `errc::operation_not_supported` if a file object cannot 308   `errc::operation_not_supported` if a file object cannot
309   use it. On Windows, the rejected handles are a pipe, a 309   use it. On Windows, the rejected handles are a pipe, a
310   socket, a console, a directory, a handle opened without 310   socket, a console, a directory, a handle opened without
311   `FILE_FLAG_OVERLAPPED`, or one 311   `FILE_FLAG_OVERLAPPED`, or one
312   already in skip-completion-port-on-success mode. On 312   already in skip-completion-port-on-success mode. On
313   Windows, `errc::invalid_argument` when @p handle is 313   Windows, `errc::invalid_argument` when @p handle is
314   bound to another completion port. Any other failure is 314   bound to another completion port. Any other failure is
315   the code reported by the system. Otherwise, the code is 315   the code reported by the system. Otherwise, the code is
316   empty. 316   empty.
317   317  
318   @par Exception Safety 318   @par Exception Safety
319   Throws nothing. On failure the object is unchanged and the 319   Throws nothing. On failure the object is unchanged and the
320   caller still owns @p handle. 320   caller still owns @p handle.
321   321  
322   @note On POSIX, the rejected descriptors are, in practice, a 322   @note On POSIX, the rejected descriptors are, in practice, a
323   directory, a pipe, a socket, or any other anonymous inode. 323   directory, a pipe, a socket, or any other anonymous inode.
324   Adopt a pipe, a socket, or an anonymous inode into a 324   Adopt a pipe, a socket, or an anonymous inode into a
325   @ref posix_stream_descriptor instead. `assign()` accepts a 325   @ref posix_stream_descriptor instead. `assign()` accepts a
326   non-seekable character device such as a tty. Its first 326   non-seekable character device such as a tty. Its first
327   read or write then fails with `ESPIPE` on the epoll, 327   read or write then fails with `ESPIPE` on the epoll,
328   kqueue, and select I/O backends. 328   kqueue, and select I/O backends.
329   329  
330   @see @ref release 330   @see @ref release
331   */ 331   */
332   [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept; 332   [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
333   333  
334   /** Move the file position. 334   /** Move the file position.
335   335  
336   Positions beyond the end of the file are allowed. A 336   Positions beyond the end of the file are allowed. A
337   resulting negative position is reported through the error 337   resulting negative position is reported through the error
338   code, as offsets often originate from file contents. A 338   code, as offsets often originate from file contents. A
339   closed file reports `errc::bad_file_descriptor`. 339   closed file reports `errc::bad_file_descriptor`.
340   340  
341   @param offset Signed offset from @p origin. 341   @param offset Signed offset from @p origin.
342   @param origin The reference point for the seek. 342   @param origin The reference point for the seek.
343   343  
344   @return The error code and new absolute position. 344   @return The error code and new absolute position.
345   */ 345   */
346   [[nodiscard]] capy::results<std::uint64_t> seek( 346   [[nodiscard]] capy::results<std::uint64_t> seek(
347   std::int64_t offset, 347   std::int64_t offset,
348   file_base::seek_basis origin = file_base::seek_set) noexcept; 348   file_base::seek_basis origin = file_base::seek_set) noexcept;
349   349  
350   protected: 350   protected:
351   /// Default-construct (for derived types that initialize `io_object` directly). 351   /// Default-construct (for derived types that initialize `io_object` directly).
HITCBC 352   16 stream_file() noexcept = default; 352   16 stream_file() noexcept = default;
353   353  
354   /** Construct from a pre-built handle (for `native_stream_file`). 354   /** Construct from a pre-built handle (for `native_stream_file`).
355   355  
356   @param h The pre-built handle to adopt. 356   @param h The pre-built handle to adopt.
357   */ 357   */
358   explicit stream_file(handle h) noexcept : io_object(std::move(h)) {} 358   explicit stream_file(handle h) noexcept : io_object(std::move(h)) {}
359   359  
360   private: 360   private:
HITCBC 361   4716 inline implementation& get() const noexcept 361   4716 inline implementation& get() const noexcept
362   { 362   {
HITCBC 363   4716 return *static_cast<implementation*>(h_.get()); 363   4716 return *static_cast<implementation*>(h_.get());
364   } 364   }
365   }; 365   };
366   366  
367   } // namespace boost::corosio 367   } // namespace boost::corosio
368   368  
369   #endif // BOOST_COROSIO_STREAM_FILE_HPP 369   #endif // BOOST_COROSIO_STREAM_FILE_HPP