1use 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
13pub struct NativeFile {
18 file: File,
19}
20
21impl NativeFile {
22 #[must_use]
24 pub const fn from_file(file: File) -> Self {
25 Self { file }
26 }
27
28 pub fn set_len(&self, size: u64) -> io::Result<()> {
30 self.file.set_len(size)
31 }
32
33 #[must_use]
35 pub const fn as_file(&self) -> &File {
36 &self.file
37 }
38
39 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 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#[derive(Clone)]
66pub struct OwnershipAnchor {
67 _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 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 #[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
114pub struct OwnershipReleaseBarrier {
119 owner: Weak<dyn Send + Sync>,
120 released: Arc<(Mutex<()>, Condvar)>,
121}
122
123impl OwnershipReleaseBarrier {
124 #[must_use]
126 pub fn is_released(&self) -> bool {
127 self.owner.strong_count() == 0
128 }
129
130 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub enum Durability {
147 Full,
149 Barrier,
154}
155
156pub 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
164pub 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
172pub fn sync_parent(path: &Path, durability: Durability) -> io::Result<()> {
174 sync_parent_impl(path, durability)
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub enum RenameMode {
180 NoReplace,
182 Replace,
184}
185
186pub fn durable_rename(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
188 durable_rename_impl(from, to, mode)
189}
190
191pub fn read_at(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
193 read_at_impl(file, offset, destination)
194}
195
196pub fn write_all_at(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
198 write_all_at_impl(file, offset, bytes)
199}
200
201#[cfg(windows)]
203pub fn set_file_attributes(path: &Path, attributes: u32) -> io::Result<()> {
204 windows::set_file_attributes(path, attributes)
205}
206
207#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct OwnedWrite {
210 pub offset: u64,
212 pub bytes: Bytes,
214}
215
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub struct OwnedRead {
219 pub offset: u64,
221 pub length: usize,
223}
224
225pub fn read_batch_async(file: File, reads: Vec<OwnedRead>) -> ReadBatch {
231 ReadBatch::submit(file, reads)
232}
233
234pub fn write_all_batch_async(file: File, writes: Vec<OwnedWrite>) -> WriteBatch {
239 WriteBatch::submit(file, writes)
240}
241
242pub 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 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
283pub fn spawn_process_tree(command: &mut std::process::Command) -> io::Result<ProcessTree> {
290 ProcessTree::spawn(command)
291}
292
293pub struct ReadBatch {
295 state: Arc<Mutex<ReadBatchState>>,
296 pending: Option<NativeJob>,
297 waiter: Option<u64>,
298}
299
300pub 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
635pub struct RangeReader<'a> {
637 file: &'a File,
638 offset: u64,
639 remaining: u64,
640}
641
642pub struct AsyncRangeReader {
644 file: File,
645 offset: u64,
646 remaining: u64,
647}
648
649impl AsyncRangeReader {
650 pub const fn new(file: File, offset: u64, length: u64) -> Self {
652 Self {
653 file,
654 offset,
655 remaining: length,
656 }
657 }
658
659 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 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 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 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 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}