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