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