Skip to main content

acyclic_native_runtime/
lib.rs

1//! Private native file primitives shared by durable local providers.
2
3use bytes::Bytes;
4use std::fs::File;
5use std::future::Future;
6use std::io::{self, Read};
7use std::path::Path;
8use std::pin::Pin;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak, mpsc};
11use std::task::{Context, Poll, Waker};
12
13/// One file owned by the native I/O runtime.
14///
15/// Callers retain responsibility for capability-safe path resolution and pass
16/// the resulting handle here exactly once.
17pub struct NativeFile {
18    file: File,
19}
20
21impl NativeFile {
22    /// Transfers ownership of an already-authorized file handle to the runtime.
23    #[must_use]
24    pub const fn from_file(file: File) -> Self {
25        Self { file }
26    }
27
28    /// Changes the logical file length.
29    pub fn set_len(&self, size: u64) -> io::Result<()> {
30        self.file.set_len(size)
31    }
32
33    /// Borrows the underlying handle for platform-specific control operations.
34    #[must_use]
35    pub const fn as_file(&self) -> &File {
36        &self.file
37    }
38
39    /// Submits owned writes while retaining this handle for later operations.
40    pub fn write_all_batch_async(&self, writes: Vec<OwnedWrite>) -> io::Result<WriteBatch> {
41        Ok(write_all_batch_async(self.file.try_clone()?, writes))
42    }
43
44    /// Flushes file contents and metadata according to `durability`.
45    pub fn sync(&self, durability: Durability) -> io::Result<()> {
46        sync_file(&self.file, durability)
47    }
48}
49
50#[cfg(target_vendor = "apple")]
51mod apple;
52#[cfg(target_os = "linux")]
53mod linux;
54mod process_tree;
55#[cfg(windows)]
56mod windows;
57
58pub use process_tree::ProcessTree;
59
60/// Shared lifetime token for native resources that must not outlive an external owner.
61///
62/// Cloning the token extends the owner's lifetime. The owned value is released only after the
63/// final token is dropped, allowing layered providers to carry a process-local ownership gate
64/// alongside every independently cloned native handle.
65#[derive(Clone)]
66pub struct OwnershipAnchor {
67    // Keep this field before `release`: Rust drops fields in declaration order, so the notifier
68    // observes the owner count after this anchor has released its strong reference.
69    _owner: Arc<dyn Send + Sync>,
70    release: OwnershipReleaseNotifier,
71}
72
73#[derive(Clone)]
74struct OwnershipReleaseNotifier {
75    owner: Weak<dyn Send + Sync>,
76    released: Arc<(Mutex<()>, Condvar)>,
77}
78
79impl Drop for OwnershipReleaseNotifier {
80    fn drop(&mut self) {
81        if self.owner.strong_count() == 0 {
82            let (lock, released) = &*self.released;
83            let _guard = lock
84                .lock()
85                .unwrap_or_else(std::sync::PoisonError::into_inner);
86            released.notify_all();
87        }
88    }
89}
90
91impl OwnershipAnchor {
92    /// Erases and retains one ownership value behind a cloneable lifetime token.
93    pub fn new(owner: impl Send + Sync + 'static) -> Self {
94        let owner: Arc<dyn Send + Sync> = Arc::new(owner);
95        Self {
96            release: OwnershipReleaseNotifier {
97                owner: Arc::downgrade(&owner),
98                released: Arc::new((Mutex::new(()), Condvar::new())),
99            },
100            _owner: owner,
101        }
102    }
103
104    /// Observes the exact boundary at which every clone has released the owner.
105    #[must_use]
106    pub fn release_barrier(&self) -> OwnershipReleaseBarrier {
107        OwnershipReleaseBarrier {
108            owner: self.release.owner.clone(),
109            released: Arc::clone(&self.release.released),
110        }
111    }
112}
113
114/// A non-owning release condition for an [`OwnershipAnchor`] clone family.
115///
116/// The barrier never extends the protected owner's lifetime. Waiting completes only after the
117/// final anchor clone has dropped the owner, making it suitable for explicit shutdown boundaries.
118pub struct OwnershipReleaseBarrier {
119    owner: Weak<dyn Send + Sync>,
120    released: Arc<(Mutex<()>, Condvar)>,
121}
122
123impl OwnershipReleaseBarrier {
124    /// Returns whether every ownership-anchor clone has released the owner.
125    #[must_use]
126    pub fn is_released(&self) -> bool {
127        self.owner.strong_count() == 0
128    }
129
130    /// Blocks until every ownership-anchor clone has released the owner.
131    pub fn wait(self) {
132        let (lock, released) = &*self.released;
133        let mut guard = lock
134            .lock()
135            .unwrap_or_else(std::sync::PoisonError::into_inner);
136        while self.owner.strong_count() != 0 {
137            guard = released
138                .wait(guard)
139                .unwrap_or_else(std::sync::PoisonError::into_inner);
140        }
141    }
142}
143
144/// Required durability boundary for one file operation.
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub enum Durability {
147    /// Flush file contents and metadata to stable storage.
148    Full,
149    /// Order earlier writes before later writes without forcing the device cache to drain.
150    ///
151    /// This capability is currently exposed only where the operating system provides that
152    /// exact primitive. Other targets return [`io::ErrorKind::Unsupported`].
153    Barrier,
154}
155
156/// Flushes file contents and metadata according to `durability`.
157pub fn sync_file(file: &File, durability: Durability) -> io::Result<()> {
158    match durability {
159        Durability::Full => file.sync_all(),
160        Durability::Barrier => barrier_sync(file),
161    }
162}
163
164/// Flushes file data according to `durability`.
165pub fn sync_data(file: &File, durability: Durability) -> io::Result<()> {
166    match durability {
167        Durability::Full => file.sync_data(),
168        Durability::Barrier => barrier_sync(file),
169    }
170}
171
172/// Flushes the directory entry namespace on platforms exposing directory flushes.
173pub fn sync_parent(path: &Path, durability: Durability) -> io::Result<()> {
174    sync_parent_impl(path, durability)
175}
176
177/// Destination behavior for one durable rename.
178#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub enum RenameMode {
180    /// Fail atomically when the destination already exists.
181    NoReplace,
182    /// Atomically replace a compatible destination entry.
183    Replace,
184}
185
186/// Renames one filesystem entry and durably publishes the affected namespace.
187pub fn durable_rename(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
188    durable_rename_impl(from, to, mode)
189}
190
191/// Reads at an absolute offset without changing the file cursor.
192pub fn read_at(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
193    read_at_impl(file, offset, destination)
194}
195
196/// Writes every byte at an absolute offset without changing the file cursor.
197pub fn write_all_at(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
198    write_all_at_impl(file, offset, bytes)
199}
200
201/// Applies the exact native Windows file-attribute bitset.
202#[cfg(windows)]
203pub fn set_file_attributes(path: &Path, attributes: u32) -> io::Result<()> {
204    windows::set_file_attributes(path, attributes)
205}
206
207/// One owned positional write whose buffer remains live through completion.
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct OwnedWrite {
210    /// Absolute file offset for the first byte.
211    pub offset: u64,
212    /// Bytes written completely or reported as an error.
213    pub bytes: Bytes,
214}
215
216/// One owned positional read request.
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub struct OwnedRead {
219    /// Absolute file offset for the first byte.
220    pub offset: u64,
221    /// Maximum bytes returned for this request.
222    pub length: usize,
223}
224
225/// Submits an owned read batch to the shared native completion worker.
226///
227/// The first poll transfers ownership to the worker if capacity is available.
228/// Dropping before admission discards the job; dropping after admission only
229/// detaches the observer and the worker completes the operation.
230pub fn read_batch_async(file: File, reads: Vec<OwnedRead>) -> ReadBatch {
231    ReadBatch::submit(file, reads)
232}
233
234/// Submits an owned write batch to the shared native completion workers.
235///
236/// Admission is irrevocable: after the first successful submission, dropping
237/// the returned future does not roll back or cancel the write.
238pub fn write_all_batch_async(file: File, writes: Vec<OwnedWrite>) -> WriteBatch {
239    WriteBatch::submit(file, writes)
240}
241
242/// Starts the current executable's `__service` mode without inheriting host
243/// standard-I/O handles.
244///
245/// On Windows this uses a detached process group and disables handle
246/// inheritance at process creation, preventing a durable service from keeping
247/// a short-lived hook runner's pipes open.
248pub fn spawn_service_process(executable: &Path) -> io::Result<()> {
249    #[cfg(windows)]
250    {
251        windows::spawn_service_process(executable)
252    }
253    #[cfg(unix)]
254    {
255        use std::os::unix::process::CommandExt as _;
256
257        let mut command = std::process::Command::new(executable);
258        command
259            .arg("__service")
260            .stdin(std::process::Stdio::null())
261            .stdout(std::process::Stdio::null())
262            .stderr(std::process::Stdio::null());
263        // A fresh process group keeps the independently durable service outside the caller's
264        // process-tree containment so client teardown cannot substitute for service drain.
265        command.process_group(0);
266        command.spawn().map(drop)
267    }
268    #[cfg(not(any(windows, unix)))]
269    {
270        use std::os::unix::process::CommandExt as _;
271
272        let mut command = std::process::Command::new(executable);
273        command
274            .arg("__service")
275            .stdin(std::process::Stdio::null())
276            .stdout(std::process::Stdio::null())
277            .stderr(std::process::Stdio::null())
278            .process_group(0);
279        command.spawn().map(drop)
280    }
281}
282
283/// Starts a child in an owned operating-system process tree.
284///
285/// Descendants inherit the Windows Job or Unix process group. Dropping the
286/// returned guard, or explicitly terminating it, kills that containment. Unix
287/// descendants can deliberately escape by creating another group or session,
288/// so independently durable services require their own lifecycle ownership.
289pub fn spawn_process_tree(command: &mut std::process::Command) -> io::Result<ProcessTree> {
290    ProcessTree::spawn(command)
291}
292
293/// Runtime-independent future for one owned native read batch.
294pub struct ReadBatch {
295    state: Arc<Mutex<ReadBatchState>>,
296    pending: Option<NativeJob>,
297    waiter: Option<u64>,
298}
299
300/// Runtime-independent future for one owned native write batch.
301pub struct WriteBatch {
302    state: Arc<Mutex<WriteBatchState>>,
303    pending: Option<NativeJob>,
304    waiter: Option<u64>,
305}
306
307struct ReadBatchState {
308    result: Option<io::Result<Vec<Bytes>>>,
309    waker: Option<Waker>,
310}
311
312struct WriteBatchState {
313    result: Option<io::Result<()>>,
314    waker: Option<Waker>,
315}
316
317enum NativeJob {
318    Read {
319        file: File,
320        reads: Vec<OwnedRead>,
321        state: Arc<Mutex<ReadBatchState>>,
322    },
323    Write {
324        file: File,
325        writes: Vec<OwnedWrite>,
326        state: Arc<Mutex<WriteBatchState>>,
327    },
328}
329
330impl NativeJob {
331    fn run(self) -> Option<Waker> {
332        match self {
333            Self::Read { file, reads, state } => {
334                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
335                    read_batch_impl(&file, &reads)
336                }))
337                .unwrap_or_else(|_| Err(io::Error::other("native read worker panicked")));
338                let mut state = state
339                    .lock()
340                    .unwrap_or_else(std::sync::PoisonError::into_inner);
341                state.result = Some(result);
342                state.waker.take()
343            }
344            Self::Write {
345                file,
346                writes,
347                state,
348            } => {
349                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
350                    write_all_batch_owned(&file, writes)
351                }))
352                .unwrap_or_else(|_| Err(io::Error::other("native write worker panicked")));
353                let mut state = state
354                    .lock()
355                    .unwrap_or_else(std::sync::PoisonError::into_inner);
356                state.result = Some(result);
357                state.waker.take()
358            }
359        }
360    }
361}
362
363struct NativeWorkers {
364    sender: mpsc::SyncSender<NativeJob>,
365    capacity_waiters: Arc<Mutex<Vec<(u64, Waker)>>>,
366    next_waiter: AtomicU64,
367}
368
369impl ReadBatch {
370    fn submit(file: File, reads: Vec<OwnedRead>) -> Self {
371        let state = Arc::new(Mutex::new(ReadBatchState {
372            result: None,
373            waker: None,
374        }));
375        Self {
376            pending: Some(NativeJob::Read {
377                file,
378                reads,
379                state: Arc::clone(&state),
380            }),
381            state,
382            waiter: None,
383        }
384    }
385}
386
387impl WriteBatch {
388    fn submit(file: File, writes: Vec<OwnedWrite>) -> Self {
389        let state = Arc::new(Mutex::new(WriteBatchState {
390            result: None,
391            waker: None,
392        }));
393        Self {
394            pending: Some(NativeJob::Write {
395                file,
396                writes,
397                state: Arc::clone(&state),
398            }),
399            state,
400            waiter: None,
401        }
402    }
403}
404
405impl Future for ReadBatch {
406    type Output = io::Result<Vec<Bytes>>;
407
408    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
409        let this = self.get_mut();
410        match poll_submission(&mut this.pending, &mut this.waiter, context) {
411            Poll::Ready(result) => result?,
412            Poll::Pending => return Poll::Pending,
413        }
414        let mut state = this
415            .state
416            .lock()
417            .unwrap_or_else(std::sync::PoisonError::into_inner);
418        if let Some(result) = state.result.take() {
419            return Poll::Ready(result);
420        }
421        if !state
422            .waker
423            .as_ref()
424            .is_some_and(|waker| waker.will_wake(context.waker()))
425        {
426            state.waker = Some(context.waker().clone());
427        }
428        Poll::Pending
429    }
430}
431
432impl Future for WriteBatch {
433    type Output = io::Result<()>;
434
435    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
436        let this = self.get_mut();
437        match poll_submission(&mut this.pending, &mut this.waiter, context) {
438            Poll::Ready(result) => result?,
439            Poll::Pending => return Poll::Pending,
440        }
441        let mut state = this
442            .state
443            .lock()
444            .unwrap_or_else(std::sync::PoisonError::into_inner);
445        if let Some(result) = state.result.take() {
446            return Poll::Ready(result);
447        }
448        if !state
449            .waker
450            .as_ref()
451            .is_some_and(|waker| waker.will_wake(context.waker()))
452        {
453            state.waker = Some(context.waker().clone());
454        }
455        Poll::Pending
456    }
457}
458
459fn poll_submission(
460    pending: &mut Option<NativeJob>,
461    waiter: &mut Option<u64>,
462    context: &Context<'_>,
463) -> Poll<io::Result<()>> {
464    let Some(job) = pending.take() else {
465        return Poll::Ready(Ok(()));
466    };
467    let workers = match native_workers() {
468        Ok(workers) => workers,
469        Err(error) => return Poll::Ready(Err(error)),
470    };
471    match workers.sender.try_send(job) {
472        Ok(()) => {
473            remove_capacity_waiter(workers, waiter.take());
474            Poll::Ready(Ok(()))
475        }
476        Err(mpsc::TrySendError::Full(job)) => {
477            let identity = *waiter
478                .get_or_insert_with(|| workers.next_waiter.fetch_add(1, Ordering::Relaxed).max(1));
479            let mut waiters = workers
480                .capacity_waiters
481                .lock()
482                .unwrap_or_else(std::sync::PoisonError::into_inner);
483            if let Some((_, waker)) = waiters
484                .iter_mut()
485                .find(|(candidate, _)| *candidate == identity)
486            {
487                waker.clone_from(context.waker());
488            } else {
489                waiters.push((identity, context.waker().clone()));
490            }
491            match workers.sender.try_send(job) {
492                Ok(()) => {
493                    let submitted = waiter.take();
494                    waiters.retain(|(candidate, _)| Some(*candidate) != submitted);
495                    Poll::Ready(Ok(()))
496                }
497                Err(mpsc::TrySendError::Full(job)) => {
498                    *pending = Some(job);
499                    Poll::Pending
500                }
501                Err(mpsc::TrySendError::Disconnected(_)) => Poll::Ready(Err(io::Error::new(
502                    io::ErrorKind::BrokenPipe,
503                    "native I/O workers stopped",
504                ))),
505            }
506        }
507        Err(mpsc::TrySendError::Disconnected(_)) => Poll::Ready(Err(io::Error::new(
508            io::ErrorKind::BrokenPipe,
509            "native I/O workers stopped",
510        ))),
511    }
512}
513
514fn remove_capacity_waiter(workers: &NativeWorkers, waiter: Option<u64>) {
515    let Some(waiter) = waiter else {
516        return;
517    };
518    workers
519        .capacity_waiters
520        .lock()
521        .unwrap_or_else(std::sync::PoisonError::into_inner)
522        .retain(|(identity, _)| *identity != waiter);
523}
524
525fn native_workers() -> io::Result<&'static NativeWorkers> {
526    static WORKERS: OnceLock<io::Result<NativeWorkers>> = OnceLock::new();
527    WORKERS
528        .get_or_init(|| {
529            let worker_count = std::thread::available_parallelism()
530                .map_or(1, std::num::NonZero::get)
531                .min(4);
532            let (sender, receiver) = mpsc::sync_channel::<NativeJob>(worker_count * 4);
533            let receiver = Arc::new(Mutex::new(receiver));
534            let capacity_waiters = Arc::new(Mutex::new(Vec::<(u64, Waker)>::new()));
535            for worker_index in 0..worker_count {
536                let receiver = Arc::clone(&receiver);
537                let capacity_waiters = Arc::clone(&capacity_waiters);
538                std::thread::Builder::new()
539                    .name(format!("acyclic-native-io-{worker_index}"))
540                    .spawn(move || {
541                        loop {
542                            let job = {
543                                let receiver = receiver
544                                    .lock()
545                                    .unwrap_or_else(std::sync::PoisonError::into_inner);
546                                receiver.recv()
547                            };
548                            let Ok(job) = job else {
549                                break;
550                            };
551                            let waiters = {
552                                let mut waiters = capacity_waiters
553                                    .lock()
554                                    .unwrap_or_else(std::sync::PoisonError::into_inner);
555                                std::mem::take(&mut *waiters)
556                            };
557                            for (_, waker) in waiters {
558                                let _ =
559                                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
560                                        waker.wake();
561                                    }));
562                            }
563                            let waker = job.run();
564                            if let Some(waker) = waker {
565                                let _ =
566                                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
567                                        waker.wake();
568                                    }));
569                            }
570                            let waiters = {
571                                let mut waiters = capacity_waiters
572                                    .lock()
573                                    .unwrap_or_else(std::sync::PoisonError::into_inner);
574                                std::mem::take(&mut *waiters)
575                            };
576                            for (_, waker) in waiters {
577                                let _ =
578                                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
579                                        waker.wake();
580                                    }));
581                            }
582                        }
583                    })?;
584            }
585            Ok(NativeWorkers {
586                sender,
587                capacity_waiters,
588                next_waiter: AtomicU64::new(1),
589            })
590        })
591        .as_ref()
592        .map_err(|error| io::Error::new(error.kind(), error.to_string()))
593}
594
595impl Drop for ReadBatch {
596    fn drop(&mut self) {
597        self.state
598            .lock()
599            .unwrap_or_else(std::sync::PoisonError::into_inner)
600            .waker = None;
601        let Some(waiter) = self.waiter else {
602            return;
603        };
604        let Ok(workers) = native_workers() else {
605            return;
606        };
607        let mut waiters = workers
608            .capacity_waiters
609            .lock()
610            .unwrap_or_else(std::sync::PoisonError::into_inner);
611        waiters.retain(|(identity, _)| *identity != waiter);
612    }
613}
614
615impl Drop for WriteBatch {
616    fn drop(&mut self) {
617        self.state
618            .lock()
619            .unwrap_or_else(std::sync::PoisonError::into_inner)
620            .waker = None;
621        let Some(waiter) = self.waiter else {
622            return;
623        };
624        let Ok(workers) = native_workers() else {
625            return;
626        };
627        let mut waiters = workers
628            .capacity_waiters
629            .lock()
630            .unwrap_or_else(std::sync::PoisonError::into_inner);
631        waiters.retain(|(identity, _)| *identity != waiter);
632    }
633}
634
635/// Bounded positional source for one file range.
636pub struct RangeReader<'a> {
637    file: &'a File,
638    offset: u64,
639    remaining: u64,
640}
641
642/// Bounded asynchronous positional source backed by the native completion path.
643pub struct AsyncRangeReader {
644    file: File,
645    offset: u64,
646    remaining: u64,
647}
648
649impl AsyncRangeReader {
650    /// Creates a source for at most `length` bytes starting at `offset`.
651    pub const fn new(file: File, offset: u64, length: u64) -> Self {
652        Self {
653            file,
654            offset,
655            remaining: length,
656        }
657    }
658
659    /// Reads the next bounded chunk without blocking the caller's executor.
660    pub async fn read(&mut self, maximum: usize) -> io::Result<Bytes> {
661        let length = usize::try_from(self.remaining.min(maximum as u64))
662            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "range too large"))?;
663        if length == 0 {
664            return Ok(Bytes::new());
665        }
666        let mut result = read_batch_async(
667            self.file.try_clone()?,
668            vec![OwnedRead {
669                offset: self.offset,
670                length,
671            }],
672        )
673        .await?;
674        let bytes = result
675            .pop()
676            .ok_or_else(|| io::Error::other("native read returned no result"))?;
677        self.offset = self
678            .offset
679            .checked_add(bytes.len() as u64)
680            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "read offset overflow"))?;
681        self.remaining -= bytes.len() as u64;
682        Ok(bytes)
683    }
684}
685
686impl<'a> RangeReader<'a> {
687    /// Creates a source for at most `length` bytes starting at `offset`.
688    pub const fn new(file: &'a File, offset: u64, length: u64) -> Self {
689        Self {
690            file,
691            offset,
692            remaining: length,
693        }
694    }
695}
696
697impl Read for RangeReader<'_> {
698    fn read(&mut self, destination: &mut [u8]) -> io::Result<usize> {
699        let maximum = usize::try_from(self.remaining.min(destination.len() as u64))
700            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "range too large"))?;
701        if maximum == 0 {
702            return Ok(0);
703        }
704        let selected = destination.get_mut(..maximum).ok_or_else(|| {
705            io::Error::new(io::ErrorKind::InvalidInput, "range exceeds destination")
706        })?;
707        let count = read_at(self.file, self.offset, selected)?;
708        self.offset = self
709            .offset
710            .checked_add(count as u64)
711            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "read offset overflow"))?;
712        self.remaining -= count as u64;
713        Ok(count)
714    }
715}
716
717#[cfg(target_os = "linux")]
718fn read_at_impl(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
719    linux::read_at(file, offset, destination)
720}
721
722#[cfg(target_os = "linux")]
723fn write_all_at_impl(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
724    linux::write_all_at(file, offset, bytes)
725}
726
727#[cfg(target_os = "linux")]
728fn write_all_batch_owned(file: &File, writes: Vec<OwnedWrite>) -> io::Result<()> {
729    linux::write_all_batch_owned(file, writes)
730}
731
732#[cfg(target_os = "linux")]
733fn read_batch_impl(file: &File, reads: &[OwnedRead]) -> io::Result<Vec<Bytes>> {
734    linux::read_batch(file, reads)
735}
736
737#[cfg(all(unix, not(target_os = "linux")))]
738fn read_at_impl(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
739    use std::os::unix::fs::FileExt as _;
740    file.read_at(destination, offset)
741}
742
743#[cfg(all(unix, not(target_os = "linux")))]
744fn write_all_at_impl(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
745    use std::os::unix::fs::FileExt as _;
746    file.write_all_at(bytes, offset)
747}
748
749#[cfg(all(unix, not(target_os = "linux")))]
750fn write_all_batch_owned(file: &File, writes: Vec<OwnedWrite>) -> io::Result<()> {
751    apple::write_all_batch_owned(file, writes)
752}
753
754#[cfg(all(unix, not(target_os = "linux")))]
755fn read_batch_impl(file: &File, reads: &[OwnedRead]) -> io::Result<Vec<Bytes>> {
756    apple::read_batch(file, reads)
757}
758
759#[cfg(windows)]
760fn read_at_impl(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
761    use std::os::windows::fs::FileExt as _;
762    file.seek_read(destination, offset)
763}
764
765#[cfg(windows)]
766fn write_all_at_impl(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
767    use std::os::windows::fs::FileExt as _;
768    let mut remaining = bytes;
769    let mut position = offset;
770    while !remaining.is_empty() {
771        let count = file.seek_write(remaining, position)?;
772        if count == 0 {
773            return Err(io::Error::new(
774                io::ErrorKind::WriteZero,
775                "positional write returned zero",
776            ));
777        }
778        position = position
779            .checked_add(count as u64)
780            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "write offset overflow"))?;
781        remaining = remaining.get(count..).ok_or_else(|| {
782            io::Error::new(
783                io::ErrorKind::InvalidData,
784                "write exceeded submitted length",
785            )
786        })?;
787    }
788    Ok(())
789}
790
791#[cfg(windows)]
792fn write_all_batch_owned(file: &File, writes: Vec<OwnedWrite>) -> io::Result<()> {
793    windows::write_all_batch_owned(file, writes)
794}
795
796#[cfg(windows)]
797fn read_batch_impl(file: &File, reads: &[OwnedRead]) -> io::Result<Vec<Bytes>> {
798    windows::read_batch(file, reads)
799}
800
801#[cfg(target_vendor = "apple")]
802#[allow(unsafe_code)]
803fn barrier_sync(file: &File) -> io::Result<()> {
804    use std::os::fd::AsRawFd as _;
805    // SAFETY: `F_BARRIERFSYNC` acts only on the live descriptor.
806    if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_BARRIERFSYNC) } == 0 {
807        Ok(())
808    } else {
809        Err(io::Error::last_os_error())
810    }
811}
812
813#[cfg(not(target_vendor = "apple"))]
814fn barrier_sync(_file: &File) -> io::Result<()> {
815    Err(io::Error::new(
816        io::ErrorKind::Unsupported,
817        "ordered write barrier is unavailable on this platform",
818    ))
819}
820
821#[cfg(unix)]
822fn sync_parent_impl(path: &Path, durability: Durability) -> io::Result<()> {
823    sync_file(&File::open(path)?, durability)
824}
825
826#[cfg(windows)]
827fn sync_parent_impl(path: &Path, durability: Durability) -> io::Result<()> {
828    use std::fs::OpenOptions;
829    use std::os::windows::fs::OpenOptionsExt as _;
830
831    const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
832    const FILE_SHARE_READ_WRITE_DELETE: u32 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004;
833    let directory = OpenOptions::new()
834        .read(true)
835        .write(true)
836        .share_mode(FILE_SHARE_READ_WRITE_DELETE)
837        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
838        .open(path)?;
839    sync_file(&directory, durability)
840}
841
842#[cfg(target_os = "linux")]
843#[allow(unsafe_code, reason = "renameat2 receives two live C paths")]
844fn durable_rename_impl(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
845    use std::os::unix::ffi::OsStrExt as _;
846    let from_path = std::ffi::CString::new(from.as_os_str().as_bytes())
847        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
848    let to_path = std::ffi::CString::new(to.as_os_str().as_bytes())
849        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
850    let flags = match mode {
851        RenameMode::NoReplace => libc::RENAME_NOREPLACE,
852        RenameMode::Replace => 0,
853    };
854    let result = unsafe {
855        libc::syscall(
856            libc::SYS_renameat2,
857            libc::AT_FDCWD,
858            from_path.as_ptr(),
859            libc::AT_FDCWD,
860            to_path.as_ptr(),
861            flags,
862        )
863    };
864    if result != 0 {
865        return Err(io::Error::last_os_error());
866    }
867    sync_rename_parents(from, to)
868}
869
870#[cfg(target_os = "macos")]
871#[allow(unsafe_code, reason = "renamex_np receives two live C paths")]
872fn durable_rename_impl(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
873    use std::os::unix::ffi::OsStrExt as _;
874    let from_path = std::ffi::CString::new(from.as_os_str().as_bytes())
875        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
876    let to_path = std::ffi::CString::new(to.as_os_str().as_bytes())
877        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
878    let flags = match mode {
879        RenameMode::NoReplace => libc::RENAME_EXCL,
880        RenameMode::Replace => 0,
881    };
882    if unsafe { libc::renamex_np(from_path.as_ptr(), to_path.as_ptr(), flags) } != 0 {
883        return Err(io::Error::last_os_error());
884    }
885    sync_rename_parents(from, to)
886}
887
888#[cfg(unix)]
889fn sync_rename_parents(from: &Path, to: &Path) -> io::Result<()> {
890    fn namespace_parent(path: &Path) -> &Path {
891        path.parent()
892            .filter(|parent| !parent.as_os_str().is_empty())
893            .unwrap_or_else(|| Path::new("."))
894    }
895    let to_parent = namespace_parent(to);
896    sync_parent(to_parent, Durability::Full)?;
897    let from_parent = namespace_parent(from);
898    if from_parent != to_parent {
899        sync_parent(from_parent, Durability::Full)?;
900    }
901    Ok(())
902}
903
904#[cfg(windows)]
905#[allow(unsafe_code, reason = "MoveFileExW receives terminated UTF-16 paths")]
906fn durable_rename_impl(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
907    use std::os::windows::ffi::OsStrExt as _;
908    use windows_sys::Win32::Storage::FileSystem::{
909        MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW,
910    };
911    let mut from: Vec<u16> = from.as_os_str().encode_wide().collect();
912    let mut to: Vec<u16> = to.as_os_str().encode_wide().collect();
913    if from.contains(&0) || to.contains(&0) {
914        return Err(io::Error::new(
915            io::ErrorKind::InvalidInput,
916            "rename path contains NUL",
917        ));
918    }
919    from.push(0);
920    to.push(0);
921    let flags = MOVEFILE_WRITE_THROUGH
922        | if mode == RenameMode::Replace {
923            MOVEFILE_REPLACE_EXISTING
924        } else {
925            0
926        };
927    // SAFETY: both arguments are live, NUL-terminated UTF-16 path buffers.
928    if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), flags) } == 0 {
929        Err(io::Error::last_os_error())
930    } else {
931        Ok(())
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use std::fs::OpenOptions;
939    use std::io::{Seek as _, SeekFrom};
940    use std::sync::Arc;
941    use std::task::{Poll, Wake};
942    use std::thread::Thread;
943    use std::time::{Duration, Instant};
944
945    struct ThreadWake(Thread);
946
947    impl Wake for ThreadWake {
948        fn wake(self: Arc<Self>) {
949            self.0.unpark();
950        }
951
952        fn wake_by_ref(self: &Arc<Self>) {
953            self.0.unpark();
954        }
955    }
956
957    fn completion_waker() -> Waker {
958        Waker::from(Arc::new(ThreadWake(std::thread::current())))
959    }
960
961    #[test]
962    fn ownership_release_barrier_waits_for_the_final_clone() {
963        let anchor = OwnershipAnchor::new(());
964        let clone = anchor.clone();
965        let barrier = anchor.release_barrier();
966        drop(anchor);
967        assert!(!barrier.is_released());
968        drop(clone);
969        barrier.wait();
970    }
971
972    #[test]
973    fn durable_rename_moves_and_replaces_entries() -> io::Result<()> {
974        let directory = tempfile::tempdir()?;
975        let source = directory.path().join("source");
976        let destination = directory.path().join("destination");
977        std::fs::write(&source, b"first")?;
978        durable_rename(&source, &destination, RenameMode::NoReplace)?;
979        assert!(!source.exists());
980        assert_eq!(std::fs::read(&destination)?, b"first");
981
982        std::fs::write(&source, b"second")?;
983        durable_rename(&source, &destination, RenameMode::Replace)?;
984        assert!(!source.exists());
985        assert_eq!(std::fs::read(destination)?, b"second");
986        Ok(())
987    }
988
989    #[cfg(unix)]
990    #[test]
991    fn durable_rename_accepts_relative_sibling_paths() -> io::Result<()> {
992        let temporary = tempfile::tempdir()?;
993        let original = std::env::current_dir()?;
994        std::env::set_current_dir(temporary.path())?;
995        let result = (|| {
996            std::fs::write("from", b"relative")?;
997            durable_rename(Path::new("from"), Path::new("to"), RenameMode::NoReplace)?;
998            assert_eq!(std::fs::read("to")?, b"relative");
999            Ok(())
1000        })();
1001        std::env::set_current_dir(original)?;
1002        result
1003    }
1004
1005    #[test]
1006    fn no_replace_preserves_both_entries() -> io::Result<()> {
1007        let directory = tempfile::tempdir()?;
1008        let source = directory.path().join("source");
1009        let destination = directory.path().join("destination");
1010        std::fs::write(&source, b"source")?;
1011        std::fs::write(&destination, b"destination")?;
1012        let Err(error) = durable_rename(&source, &destination, RenameMode::NoReplace) else {
1013            return Err(io::Error::other(
1014                "existing destination accepted no-replace rename",
1015            ));
1016        };
1017        assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
1018        assert_eq!(std::fs::read(source)?, b"source");
1019        assert_eq!(std::fs::read(destination)?, b"destination");
1020        Ok(())
1021    }
1022
1023    fn complete_read(mut read: ReadBatch) -> io::Result<Vec<Bytes>> {
1024        let deadline = Instant::now() + Duration::from_secs(10);
1025        let waker = completion_waker();
1026        loop {
1027            match Pin::new(&mut read).poll(&mut Context::from_waker(&waker)) {
1028                Poll::Ready(result) => return result,
1029                Poll::Pending if Instant::now() < deadline => {
1030                    std::thread::park_timeout(deadline.saturating_duration_since(Instant::now()));
1031                }
1032                Poll::Pending => {
1033                    return Err(io::Error::new(
1034                        io::ErrorKind::TimedOut,
1035                        "owned native read did not complete",
1036                    ));
1037                }
1038            }
1039        }
1040    }
1041
1042    fn complete_write(mut write: WriteBatch) -> io::Result<()> {
1043        let deadline = Instant::now() + Duration::from_secs(10);
1044        let waker = completion_waker();
1045        loop {
1046            match Pin::new(&mut write).poll(&mut Context::from_waker(&waker)) {
1047                Poll::Ready(result) => return result,
1048                Poll::Pending if Instant::now() < deadline => {
1049                    std::thread::park_timeout(deadline.saturating_duration_since(Instant::now()));
1050                }
1051                Poll::Pending => {
1052                    return Err(io::Error::new(
1053                        io::ErrorKind::TimedOut,
1054                        "owned native write did not complete",
1055                    ));
1056                }
1057            }
1058        }
1059    }
1060
1061    #[test]
1062    fn positional_io_preserves_cursor_and_bounds_ranges() -> io::Result<()> {
1063        let temporary = tempfile::tempdir()?;
1064        let path = temporary.path().join("positional");
1065        let file = OpenOptions::new()
1066            .create_new(true)
1067            .read(true)
1068            .write(true)
1069            .open(path)?;
1070        write_all_at(&file, 2, &[0x22; 3])?;
1071        write_all_at(&file, 16, &[0x33; 5])?;
1072        sync_file(&file, Durability::Full)?;
1073
1074        let mut bytes = [0_u8; 24];
1075        assert_eq!(read_at(&file, 0, &mut bytes)?, 21);
1076        assert_eq!(&bytes[..8], &[0, 0, 0x22, 0x22, 0x22, 0, 0, 0]);
1077        assert_eq!(&bytes[8..16], &[0; 8]);
1078        assert_eq!(&bytes[16..21], &[0x33; 5]);
1079        assert_eq!(read_at(&file, 21, &mut bytes)?, 0);
1080
1081        let mut range = RangeReader::new(&file, 15, 8);
1082        let mut selected = Vec::new();
1083        range.read_to_end(&mut selected)?;
1084        assert_eq!(selected, [0, 0x33, 0x33, 0x33, 0x33, 0x33]);
1085
1086        complete_write(write_all_batch_async(
1087            file.try_clone()?,
1088            vec![
1089                OwnedWrite {
1090                    offset: 32,
1091                    bytes: Bytes::from_static(&[0x44; 5]),
1092                },
1093                OwnedWrite {
1094                    offset: 48,
1095                    bytes: Bytes::from_static(&[0x55; 7]),
1096                },
1097            ],
1098        ))?;
1099        let mut batch = [0_u8; 23];
1100        assert_eq!(read_at(&file, 32, &mut batch)?, 23);
1101        assert_eq!(&batch[..5], &[0x44; 5]);
1102        assert_eq!(&batch[5..16], &[0; 11]);
1103        assert_eq!(&batch[16..], &[0x55; 7]);
1104
1105        // Native batch reads are positional even after another handle sharing
1106        // the open file description has moved its cursor to EOF.
1107        let mut cursor = file.try_clone()?;
1108        cursor.seek(SeekFrom::End(0))?;
1109        let selected = complete_read(read_batch_async(
1110            file.try_clone()?,
1111            vec![OwnedRead {
1112                offset: 16,
1113                length: 5,
1114            }],
1115        ))?;
1116        assert_eq!(selected, [Bytes::from_static(&[0x33; 5])]);
1117        assert_eq!(cursor.stream_position()?, 55);
1118        Ok(())
1119    }
1120
1121    #[test]
1122    fn batch_larger_than_native_queue_preserves_every_write() -> io::Result<()> {
1123        let temporary = tempfile::tempdir()?;
1124        let file = OpenOptions::new()
1125            .create_new(true)
1126            .read(true)
1127            .write(true)
1128            .open(temporary.path().join("large-batch"))?;
1129        let writes: Vec<_> = (0_u8..17)
1130            .map(|value| OwnedWrite {
1131                offset: u64::from(value) * 4,
1132                bytes: Bytes::from(vec![value; 3]),
1133            })
1134            .collect();
1135        complete_write(write_all_batch_async(file.try_clone()?, writes))?;
1136
1137        let mut actual = [0_u8; 67];
1138        assert_eq!(read_at(&file, 0, &mut actual)?, actual.len());
1139        for value in 0_u8..17 {
1140            let start = usize::from(value) * 4;
1141            assert_eq!(actual.get(start..start + 3), Some([value; 3].as_slice()));
1142            if value != 16 {
1143                assert_eq!(actual.get(start + 3), Some(&0));
1144            }
1145        }
1146        Ok(())
1147    }
1148
1149    #[cfg(target_os = "linux")]
1150    #[test]
1151    fn completed_batches_preserve_ring_reuse() -> io::Result<()> {
1152        let temporary = tempfile::tempdir()?;
1153        let file = OpenOptions::new()
1154            .create_new(true)
1155            .read(true)
1156            .write(true)
1157            .open(temporary.path().join("ring-reuse"))?;
1158        for value in 0_u16..256 {
1159            complete_write(write_all_batch_async(
1160                file.try_clone()?,
1161                vec![OwnedWrite {
1162                    offset: u64::from(value),
1163                    bytes: Bytes::copy_from_slice(&[value.to_le_bytes()[0]]),
1164                }],
1165            ))?;
1166        }
1167        let contents = complete_read(read_batch_async(
1168            file,
1169            vec![OwnedRead {
1170                offset: 0,
1171                length: 256,
1172            }],
1173        ))?;
1174        assert_eq!(contents.first().map(Bytes::len), Some(256));
1175        Ok(())
1176    }
1177
1178    #[cfg(windows)]
1179    #[test]
1180    fn native_write_batch_preserves_empty_writes_and_port_reuse() -> io::Result<()> {
1181        let temporary = tempfile::tempdir()?;
1182        let file = OpenOptions::new()
1183            .create_new(true)
1184            .read(true)
1185            .write(true)
1186            .open(temporary.path().join("write-batch-reuse"))?;
1187        for value in 0_u8..8 {
1188            complete_write(write_all_batch_async(
1189                file.try_clone()?,
1190                vec![
1191                    OwnedWrite {
1192                        offset: u64::from(value) * 2,
1193                        bytes: Bytes::copy_from_slice(&[value]),
1194                    },
1195                    OwnedWrite {
1196                        offset: u64::from(value) * 2 + 1,
1197                        bytes: Bytes::new(),
1198                    },
1199                ],
1200            ))?;
1201        }
1202        let mut actual = [0_u8; 15];
1203        assert_eq!(read_at(&file, 0, &mut actual)?, actual.len());
1204        for value in 0_u8..8 {
1205            assert_eq!(actual.get(usize::from(value) * 2), Some(&value));
1206        }
1207        Ok(())
1208    }
1209
1210    #[test]
1211    fn owned_write_outlives_the_callers_file_and_buffers() -> io::Result<()> {
1212        let temporary = tempfile::tempdir()?;
1213        let path = temporary.path().join("owned-write");
1214        let file = OpenOptions::new()
1215            .create_new(true)
1216            .read(true)
1217            .write(true)
1218            .open(&path)?;
1219        let write = write_all_batch_async(
1220            file,
1221            vec![OwnedWrite {
1222                offset: 3,
1223                bytes: Bytes::from_static(b"owned"),
1224            }],
1225        );
1226        complete_write(write)?;
1227        let mut actual = Vec::new();
1228        File::open(path)?.read_to_end(&mut actual)?;
1229        assert_eq!(actual, [0, 0, 0, b'o', b'w', b'n', b'e', b'd']);
1230        Ok(())
1231    }
1232
1233    #[test]
1234    fn owned_native_file_preserves_positional_contract() -> io::Result<()> {
1235        let temporary = tempfile::tempdir()?;
1236        let path = temporary.path().join("owned-native-file");
1237        let file = OpenOptions::new()
1238            .create_new(true)
1239            .read(true)
1240            .write(true)
1241            .open(path)?;
1242        let native = NativeFile::from_file(file);
1243        native.set_len(32)?;
1244        write_all_at(native.as_file(), 7, b"native")?;
1245        complete_write(native.write_all_batch_async(vec![
1246            OwnedWrite {
1247                offset: 0,
1248                bytes: Bytes::from_static(b"one"),
1249            },
1250            OwnedWrite {
1251                offset: 20,
1252                bytes: Bytes::from_static(b"two"),
1253            },
1254        ])?)?;
1255        native.sync(Durability::Full)?;
1256
1257        let mut actual = [0_u8; 32];
1258        assert_eq!(read_at(native.as_file(), 0, &mut actual)?, actual.len());
1259        assert_eq!(actual.get(..3), Some(b"one".as_slice()));
1260        assert_eq!(actual.get(7..13), Some(b"native".as_slice()));
1261        assert_eq!(actual.get(20..23), Some(b"two".as_slice()));
1262        Ok(())
1263    }
1264
1265    #[cfg(not(target_vendor = "apple"))]
1266    #[test]
1267    fn barrier_durability_rejects_targets_without_an_exact_primitive() -> io::Result<()> {
1268        let temporary = tempfile::tempdir()?;
1269        let file = File::create(temporary.path().join("barrier"))?;
1270        assert!(matches!(
1271            sync_file(&file, Durability::Barrier),
1272            Err(error) if error.kind() == io::ErrorKind::Unsupported
1273        ));
1274        assert!(matches!(
1275            sync_data(&file, Durability::Barrier),
1276            Err(error) if error.kind() == io::ErrorKind::Unsupported
1277        ));
1278        Ok(())
1279    }
1280
1281    #[test]
1282    fn native_read_batch_preserves_order_and_short_reads() -> io::Result<()> {
1283        let temporary = tempfile::tempdir()?;
1284        let file = OpenOptions::new()
1285            .create_new(true)
1286            .read(true)
1287            .write(true)
1288            .open(temporary.path().join("read-batch"))?;
1289        write_all_at(&file, 0, b"abcdefgh")?;
1290        let reads = complete_read(read_batch_async(
1291            file,
1292            vec![
1293                OwnedRead {
1294                    offset: 5,
1295                    length: 8,
1296                },
1297                OwnedRead {
1298                    offset: 1,
1299                    length: 3,
1300                },
1301            ],
1302        ))?;
1303        assert_eq!(
1304            reads,
1305            [Bytes::from_static(b"fgh"), Bytes::from_static(b"bcd")]
1306        );
1307        Ok(())
1308    }
1309
1310    #[cfg(target_vendor = "apple")]
1311    #[test]
1312    fn native_batches_reject_all_invalid_offsets_before_submission() -> io::Result<()> {
1313        let temporary = tempfile::tempdir()?;
1314        let path = temporary.path().join("invalid-offset-batch");
1315        let file = OpenOptions::new()
1316            .create_new(true)
1317            .read(true)
1318            .write(true)
1319            .open(&path)?;
1320        write_all_at(&file, 0, b"stable")?;
1321
1322        let read = complete_read(read_batch_async(
1323            file.try_clone()?,
1324            vec![
1325                OwnedRead {
1326                    offset: 0,
1327                    length: 1,
1328                },
1329                OwnedRead {
1330                    offset: u64::MAX,
1331                    length: 1,
1332                },
1333            ],
1334        ));
1335        assert!(matches!(read, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
1336
1337        let write = complete_write(write_all_batch_async(
1338            file,
1339            vec![
1340                OwnedWrite {
1341                    offset: 0,
1342                    bytes: Bytes::from_static(b"changed"),
1343                },
1344                OwnedWrite {
1345                    offset: u64::MAX,
1346                    bytes: Bytes::from_static(b"x"),
1347                },
1348            ],
1349        ));
1350        assert!(matches!(write, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
1351        assert_eq!(std::fs::read(path)?, b"stable");
1352        Ok(())
1353    }
1354
1355    #[cfg(target_os = "linux")]
1356    #[test]
1357    fn io_uring_rejects_cursor_aliases_before_any_submission() -> io::Result<()> {
1358        let temporary = tempfile::tempdir()?;
1359        let path = temporary.path().join("invalid-io-uring-offset");
1360        let mut file = OpenOptions::new()
1361            .create_new(true)
1362            .read(true)
1363            .write(true)
1364            .open(&path)?;
1365        write_all_at(&file, 0, b"stable")?;
1366        file.seek(SeekFrom::Start(3))?;
1367
1368        let mut byte = [0_u8; 1];
1369        assert!(matches!(
1370            read_at(&file, u64::MAX, &mut byte),
1371            Err(error) if error.kind() == io::ErrorKind::InvalidInput
1372        ));
1373        assert!(matches!(
1374            write_all_at(&file, u64::MAX, b"x"),
1375            Err(error) if error.kind() == io::ErrorKind::InvalidInput
1376        ));
1377        assert!(matches!(
1378            complete_read(read_batch_async(
1379                file.try_clone()?,
1380                vec![
1381                    OwnedRead {
1382                        offset: 0,
1383                        length: 1,
1384                    },
1385                    OwnedRead {
1386                        offset: u64::MAX,
1387                        length: 1,
1388                    },
1389                ],
1390            )),
1391            Err(error) if error.kind() == io::ErrorKind::InvalidInput
1392        ));
1393        assert!(matches!(
1394            complete_write(write_all_batch_async(
1395                file.try_clone()?,
1396                vec![
1397                    OwnedWrite {
1398                        offset: 0,
1399                        bytes: Bytes::from_static(b"changed"),
1400                    },
1401                    OwnedWrite {
1402                        offset: u64::MAX,
1403                        bytes: Bytes::from_static(b"x"),
1404                    },
1405                ],
1406            )),
1407            Err(error) if error.kind() == io::ErrorKind::InvalidInput
1408        ));
1409        assert_eq!(file.stream_position()?, 3);
1410        assert_eq!(std::fs::read(path)?, b"stable");
1411        Ok(())
1412    }
1413
1414    #[test]
1415    fn owned_read_outlives_the_callers_file_and_request() -> io::Result<()> {
1416        let temporary = tempfile::tempdir()?;
1417        let path = temporary.path().join("owned-read");
1418        let file = OpenOptions::new()
1419            .create_new(true)
1420            .read(true)
1421            .write(true)
1422            .open(path)?;
1423        write_all_at(&file, 0, b"abcdefgh")?;
1424        let read = read_batch_async(
1425            file,
1426            vec![OwnedRead {
1427                offset: 2,
1428                length: 4,
1429            }],
1430        );
1431        let actual = complete_read(read)?;
1432        assert_eq!(actual, [Bytes::from_static(b"cdef")]);
1433        Ok(())
1434    }
1435
1436    #[test]
1437    fn admitted_write_outlives_dropped_observer() -> io::Result<()> {
1438        let temporary = tempfile::tempdir()?;
1439        let path = temporary.path().join("detached-write");
1440        let file = OpenOptions::new()
1441            .create_new(true)
1442            .read(true)
1443            .write(true)
1444            .open(&path)?;
1445        let mut write = write_all_batch_async(
1446            file,
1447            vec![OwnedWrite {
1448                offset: 0,
1449                bytes: Bytes::from_static(b"persisted"),
1450            }],
1451        );
1452        let (sender, receiver) = mpsc::sync_channel(1);
1453        let admitted = write
1454            .pending
1455            .take()
1456            .ok_or_else(|| io::Error::other("write was not pending admission"))?;
1457        sender
1458            .try_send(admitted)
1459            .map_err(|_| io::Error::other("write admission failed"))?;
1460
1461        drop(write);
1462        let job = receiver
1463            .recv()
1464            .map_err(|_| io::Error::other("admitted write disappeared"))?;
1465        let _ = job.run();
1466
1467        assert_eq!(std::fs::read(path)?, b"persisted");
1468        Ok(())
1469    }
1470
1471    #[test]
1472    fn dropped_admission_waiters_do_not_block_later_reads() -> io::Result<()> {
1473        enum FirstPoll {
1474            Ready(io::Result<Vec<Bytes>>),
1475            Pending(ReadBatch),
1476        }
1477
1478        let temporary = tempfile::tempdir()?;
1479        let path = temporary.path().join("admission-drop");
1480        let file = OpenOptions::new()
1481            .create_new(true)
1482            .read(true)
1483            .write(true)
1484            .open(path)?;
1485        write_all_at(&file, 0, b"x")?;
1486        let mut reads = Vec::new();
1487        for _ in 0..64 {
1488            let mut read = read_batch_async(
1489                file.try_clone()?,
1490                vec![OwnedRead {
1491                    offset: 0,
1492                    length: 1,
1493                }],
1494            );
1495            reads.push(
1496                match Pin::new(&mut read).poll(&mut Context::from_waker(Waker::noop())) {
1497                    Poll::Ready(result) => FirstPoll::Ready(result),
1498                    Poll::Pending => FirstPoll::Pending(read),
1499                },
1500            );
1501        }
1502        for read in &reads {
1503            if let FirstPoll::Ready(result) = read {
1504                match result {
1505                    Ok(completed) => assert_eq!(completed, &[Bytes::from_static(b"x")]),
1506                    Err(error) => {
1507                        return Err(io::Error::new(
1508                            error.kind(),
1509                            format!("initial native read failed: {error}"),
1510                        ));
1511                    }
1512                }
1513            }
1514        }
1515        reads.truncate(8);
1516        for read in reads {
1517            let completed = match read {
1518                FirstPoll::Ready(result) => result?,
1519                FirstPoll::Pending(read) => complete_read(read)?,
1520            };
1521            assert_eq!(completed, [Bytes::from_static(b"x")]);
1522        }
1523        let final_read = read_batch_async(
1524            file,
1525            vec![OwnedRead {
1526                offset: 0,
1527                length: 1,
1528            }],
1529        );
1530        assert_eq!(complete_read(final_read)?, [Bytes::from_static(b"x")]);
1531        Ok(())
1532    }
1533
1534    #[cfg(windows)]
1535    #[test]
1536    fn native_read_batch_preserves_empty_and_eof_ranges() -> io::Result<()> {
1537        let temporary = tempfile::tempdir()?;
1538        let file = OpenOptions::new()
1539            .create_new(true)
1540            .read(true)
1541            .write(true)
1542            .open(temporary.path().join("read-batch-eof"))?;
1543        write_all_at(&file, 0, b"abc")?;
1544        let reads = complete_read(read_batch_async(
1545            file.try_clone()?,
1546            vec![
1547                OwnedRead {
1548                    offset: 0,
1549                    length: 0,
1550                },
1551                OwnedRead {
1552                    offset: 3,
1553                    length: 4,
1554                },
1555                OwnedRead {
1556                    offset: 30,
1557                    length: 4,
1558                },
1559            ],
1560        ))?;
1561        assert_eq!(reads, [Bytes::new(), Bytes::new(), Bytes::new()]);
1562        for (offset, expected) in b"abc".iter().copied().enumerate() {
1563            let repeated = complete_read(read_batch_async(
1564                file.try_clone()?,
1565                vec![OwnedRead {
1566                    offset: offset as u64,
1567                    length: 1,
1568                }],
1569            ))?;
1570            assert_eq!(repeated, [Bytes::copy_from_slice(&[expected])]);
1571        }
1572        Ok(())
1573    }
1574
1575    #[cfg(target_os = "linux")]
1576    #[test]
1577    fn native_read_batch_chunks_large_ranges() -> io::Result<()> {
1578        let temporary = tempfile::tempdir()?;
1579        let file = OpenOptions::new()
1580            .create_new(true)
1581            .read(true)
1582            .write(true)
1583            .open(temporary.path().join("large-read-batch"))?;
1584        let expected = Bytes::from(vec![0x5a; 1024 * 1024 + 1]);
1585        write_all_at(&file, 7, &expected)?;
1586        let actual = complete_read(read_batch_async(
1587            file,
1588            vec![OwnedRead {
1589                offset: 7,
1590                length: expected.len(),
1591            }],
1592        ))?;
1593        assert_eq!(actual, [expected]);
1594        Ok(())
1595    }
1596
1597    #[cfg(target_os = "linux")]
1598    #[test]
1599    fn native_read_batch_windows_requests_beyond_the_ring() -> io::Result<()> {
1600        let temporary = tempfile::tempdir()?;
1601        let file = OpenOptions::new()
1602            .create_new(true)
1603            .read(true)
1604            .write(true)
1605            .open(temporary.path().join("windowed-read-batch"))?;
1606        let expected: Vec<u8> = (0..17).collect();
1607        write_all_at(&file, 0, &expected)?;
1608        let requests = (0_u64..17)
1609            .map(|offset| OwnedRead { offset, length: 1 })
1610            .collect();
1611        let actual = complete_read(read_batch_async(file, requests))?;
1612        assert_eq!(
1613            actual,
1614            expected
1615                .into_iter()
1616                .map(|value| Bytes::copy_from_slice(&[value]))
1617                .collect::<Vec<_>>()
1618        );
1619        Ok(())
1620    }
1621
1622    #[cfg(target_os = "linux")]
1623    #[test]
1624    fn native_read_batch_windows_one_range_beyond_the_ring() -> io::Result<()> {
1625        let temporary = tempfile::tempdir()?;
1626        let file = OpenOptions::new()
1627            .create_new(true)
1628            .read(true)
1629            .write(true)
1630            .open(temporary.path().join("windowed-large-read"))?;
1631        let expected = Bytes::from(vec![0x69; 16 * 1024 * 1024 + 1]);
1632        write_all_at(&file, 3, &expected)?;
1633        let actual = complete_read(read_batch_async(
1634            file,
1635            vec![OwnedRead {
1636                offset: 3,
1637                length: expected.len(),
1638            }],
1639        ))?;
1640        assert_eq!(actual, [expected]);
1641        Ok(())
1642    }
1643}