1use std::path::{Path, PathBuf};
24use std::sync::Mutex;
25
26use async_trait::async_trait;
27use tracing::debug;
28
29use crate::error::{Aria2Error, Result};
30
31use super::disk_writer::SeekableDiskWriter;
32
33pub struct PositionedDiskWriter {
47 file: Mutex<Option<std::fs::File>>,
52 path: PathBuf,
53 total_size: Option<u64>,
54}
55
56impl PositionedDiskWriter {
57 pub fn new(path: &Path, total_size: Option<u64>) -> Self {
64 Self {
65 file: Mutex::new(None),
66 path: path.to_path_buf(),
67 total_size,
68 }
69 }
70
71 pub fn total_size(&self) -> Option<u64> {
73 self.total_size
74 }
75
76 fn ensure_open_sync(&self) -> Result<()> {
84 let mut guard = self
85 .file
86 .lock()
87 .map_err(|e| Aria2Error::Io(format!("file mutex poisoned: {e}")))?;
88 if guard.is_some() {
89 return Ok(());
90 }
91
92 if let Some(parent) = self.path.parent()
94 && !parent.as_os_str().is_empty()
95 && !parent.exists()
96 {
97 std::fs::create_dir_all(parent)?;
98 debug!("Created parent directories for {:?}", self.path);
99 }
100
101 let mut opts = std::fs::OpenOptions::new();
102 opts.create(true).write(true).read(true);
103 let file = opts.open(&self.path)?;
104 debug!("Opened file for positioned I/O: {:?}", self.path);
105
106 if let Some(size) = self.total_size {
110 let current_size = file.metadata()?.len();
111 if current_size == 0 && size > 0 {
112 file.set_len(size)?;
113 debug!("Pre-allocated file to {} bytes: {:?}", size, self.path);
114 }
115 }
116
117 *guard = Some(file);
118 Ok(())
119 }
120
121 fn lock_file(&self) -> Result<std::sync::MutexGuard<'_, Option<std::fs::File>>> {
123 self.file
124 .lock()
125 .map_err(|e| Aria2Error::Io(format!("file mutex poisoned: {e}")))
126 }
127
128 #[cfg(unix)]
138 pub fn raw_fd(&self) -> Option<std::os::unix::io::RawFd> {
139 let guard = self.file.lock().ok()?;
140 guard.as_ref().map(std::os::unix::io::AsRawFd::as_raw_fd)
141 }
142}
143
144#[async_trait]
145impl SeekableDiskWriter for PositionedDiskWriter {
146 async fn open(&mut self) -> Result<()> {
147 self.ensure_open_sync()
148 }
149
150 async fn write_at(&mut self, offset: u64, data: &[u8]) -> Result<()> {
151 self.ensure_open_sync()?;
152 let guard = self.lock_file()?;
153 let file = guard.as_ref().ok_or_else(|| {
154 Aria2Error::Io("file not open after ensure_open_sync — invariant violated".into())
155 })?;
156 write_all_at(file, data, offset)
157 }
158
159 async fn write_bytes_at(&mut self, offset: u64, data: bytes::Bytes) -> Result<()> {
162 self.write_at(offset, &data).await
163 }
164
165 async fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize> {
166 self.ensure_open_sync()?;
167 let guard = self.lock_file()?;
168 let file = guard.as_ref().ok_or_else(|| {
169 Aria2Error::Io("file not open after ensure_open_sync — invariant violated".into())
170 })?;
171 read_exact_at(file, buf, offset)
172 }
173
174 async fn truncate(&mut self, length: u64) -> Result<()> {
175 self.ensure_open_sync()?;
176 let guard = self.lock_file()?;
177 if let Some(ref file) = *guard {
178 file.set_len(length)?;
179 }
180 Ok(())
181 }
182
183 async fn flush(&mut self) -> Result<()> {
184 let guard = self.lock_file()?;
185 if let Some(ref file) = *guard {
186 file.sync_all()?;
190 }
191 Ok(())
192 }
193
194 async fn len(&self) -> Result<u64> {
195 let guard = self.lock_file()?;
196 if let Some(ref file) = *guard {
197 Ok(file.metadata()?.len())
198 } else if let Some(size) = self.total_size {
199 Ok(size)
200 } else {
201 Ok(0)
202 }
203 }
204
205 fn path(&self) -> &Path {
206 &self.path
207 }
208}
209
210fn write_all_at(file: &std::fs::File, mut buf: &[u8], mut offset: u64) -> Result<()> {
219 while !buf.is_empty() {
220 let n = positioned_write(file, buf, offset)?;
221 if n == 0 {
222 return Err(Aria2Error::Io(
223 "positioned write returned 0 — failed to write whole buffer".into(),
224 ));
225 }
226 offset += n as u64;
227 buf = &buf[n..];
228 }
229 Ok(())
230}
231
232fn read_exact_at(file: &std::fs::File, buf: &mut [u8], offset: u64) -> Result<usize> {
236 let mut filled = 0usize;
237 let mut current_offset = offset;
238 while filled < buf.len() {
239 let n = positioned_read(file, &mut buf[filled..], current_offset)?;
240 if n == 0 {
241 break; }
243 filled += n;
244 current_offset += n as u64;
245 }
246 Ok(filled)
247}
248
249#[cfg(unix)]
251fn positioned_write(file: &std::fs::File, buf: &[u8], offset: u64) -> Result<usize> {
252 use std::os::unix::fs::FileExt;
253 Ok(file.write_at(buf, offset)?)
254}
255
256#[cfg(unix)]
258fn positioned_read(file: &std::fs::File, buf: &mut [u8], offset: u64) -> Result<usize> {
259 use std::os::unix::fs::FileExt;
260 Ok(file.read_at(buf, offset)?)
261}
262
263#[cfg(windows)]
265fn positioned_write(file: &std::fs::File, buf: &[u8], offset: u64) -> Result<usize> {
266 use std::os::windows::fs::FileExt;
267 Ok(file.seek_write(buf, offset)?)
268}
269
270#[cfg(windows)]
272fn positioned_read(file: &std::fs::File, buf: &mut [u8], offset: u64) -> Result<usize> {
273 use std::os::windows::fs::FileExt;
274 Ok(file.seek_read(buf, offset)?)
275}
276
277#[cfg(not(any(unix, windows)))]
278fn positioned_write(_file: &std::fs::File, _buf: &[u8], _offset: u64) -> Result<usize> {
279 Err(Aria2Error::Io(
280 "positioned write not supported on this platform".into(),
281 ))
282}
283
284#[cfg(not(any(unix, windows)))]
285fn positioned_read(_file: &std::fs::File, _buf: &mut [u8], _offset: u64) -> Result<usize> {
286 Err(Aria2Error::Io(
287 "positioned read not supported on this platform".into(),
288 ))
289}
290
291pub fn create_positioned_writer(
308 path: &Path,
309 total_size: Option<u64>,
310) -> Box<dyn SeekableDiskWriter> {
311 #[cfg(all(target_os = "linux", feature = "io_uring"))]
312 {
313 Box::new(IoUringDiskWriter::new(path, total_size))
314 }
315 #[cfg(not(all(target_os = "linux", feature = "io_uring")))]
316 {
317 Box::new(PositionedDiskWriter::new(path, total_size))
318 }
319}
320
321#[cfg(test)]
326mod tests {
327 use super::*;
328 use std::sync::Arc;
329
330 #[tokio::test]
331 async fn test_positioned_write_basic() {
332 let dir = tempfile::tempdir().unwrap();
333 let path = dir.path().join("test_basic.bin");
334
335 let mut writer = PositionedDiskWriter::new(&path, Some(1024));
336 writer.open().await.unwrap();
337 writer.write_at(0, b"hello world").await.unwrap();
338 writer.flush().await.unwrap();
339
340 let mut buf = [0u8; 11];
341 let n = writer.read_at(0, &mut buf).await.unwrap();
342 assert_eq!(n, 11);
343 assert_eq!(&buf, b"hello world");
344 }
345
346 #[tokio::test]
347 async fn test_positioned_write_at_offset() {
348 let dir = tempfile::tempdir().unwrap();
349 let path = dir.path().join("test_offset.bin");
350
351 let mut writer = PositionedDiskWriter::new(&path, None);
352 writer.open().await.unwrap();
353 writer.write_at(100, b"data at 100").await.unwrap();
355 writer.flush().await.unwrap();
356
357 let mut buf = [0u8; 11];
359 let n = writer.read_at(100, &mut buf).await.unwrap();
360 assert_eq!(n, 11);
361 assert_eq!(&buf, b"data at 100");
362
363 let mut buf0 = [0xFFu8; 12];
365 let n0 = writer.read_at(0, &mut buf0).await.unwrap();
366 assert_eq!(n0, 12, "should read full 12 bytes from zero-filled region");
367 assert!(
368 buf0.iter().all(|&b| b == 0),
369 "offset 0 should be zero-filled, got {:?}",
370 buf0
371 );
372 }
373
374 #[tokio::test]
375 async fn test_positioned_writer_truncate_and_len() {
376 let dir = tempfile::tempdir().unwrap();
377 let path = dir.path().join("test_trunc.bin");
378
379 let mut writer = PositionedDiskWriter::new(&path, Some(2048));
380 writer.open().await.unwrap();
381
382 let len = writer.len().await.unwrap();
384 assert_eq!(len, 2048);
385
386 writer.truncate(512).await.unwrap();
387 let len = writer.len().await.unwrap();
388 assert_eq!(len, 512);
389 }
390
391 #[tokio::test]
392 async fn test_positioned_writer_len_before_open() {
393 let dir = tempfile::tempdir().unwrap();
394 let path = dir.path().join("test_len_before_open.bin");
395
396 let writer = PositionedDiskWriter::new(&path, Some(9999));
397 let len = writer.len().await.unwrap();
398 assert_eq!(len, 9999, "should return total_size before open");
399 }
400
401 #[tokio::test]
402 async fn test_positioned_writer_len_no_total_size_before_open() {
403 let dir = tempfile::tempdir().unwrap();
404 let path = dir.path().join("test_len_none.bin");
405
406 let writer = PositionedDiskWriter::new(&path, None);
407 let len = writer.len().await.unwrap();
408 assert_eq!(len, 0, "should return 0 before open when no total_size");
409 }
410
411 #[tokio::test]
412 async fn test_positioned_writer_resume_does_not_truncate() {
413 let dir = tempfile::tempdir().unwrap();
416 let path = dir.path().join("test_resume.bin");
417
418 {
420 let mut w = PositionedDiskWriter::new(&path, Some(1024));
421 w.open().await.unwrap();
422 w.write_at(0, b"resume-data").await.unwrap();
423 w.flush().await.unwrap();
424 }
425
426 {
428 let mut w = PositionedDiskWriter::new(&path, Some(1024));
429 w.open().await.unwrap();
430 let mut buf = [0u8; 11];
431 let n = w.read_at(0, &mut buf).await.unwrap();
432 assert_eq!(n, 11);
433 assert_eq!(&buf, b"resume-data", "existing data must survive reopen");
434 }
435 }
436
437 #[tokio::test]
438 async fn test_positioned_writer_creates_parent_dirs() {
439 let dir = tempfile::tempdir().unwrap();
440 let path = dir.path().join("nested").join("deep").join("file.bin");
441
442 let mut writer = PositionedDiskWriter::new(&path, Some(64));
443 writer.open().await.unwrap();
444 writer.write_at(0, b"x").await.unwrap();
445 writer.flush().await.unwrap();
446
447 assert!(path.exists(), "file should be created with parent dirs");
448 }
449
450 #[tokio::test]
451 async fn test_concurrent_writes_non_overlapping() {
452 let dir = tempfile::tempdir().unwrap();
457 let path = dir.path().join("test_concurrent_shared.bin");
458
459 let chunk_size: usize = 64 * 1024;
460 let num_tasks: usize = 4;
461
462 let mut writer = PositionedDiskWriter::new(&path, Some((chunk_size * num_tasks) as u64));
463 writer.open().await.unwrap();
464 let writer = Arc::new(tokio::sync::Mutex::new(writer));
465
466 let mut handles = Vec::with_capacity(num_tasks);
467 for i in 0..num_tasks {
468 let offset = (i as u64) * chunk_size as u64;
469 let fill = (i as u8) + 1;
470 let data = bytes::Bytes::from(vec![fill; chunk_size]);
471 let w = writer.clone();
472 handles.push(tokio::spawn(async move {
473 let mut guard = w.lock().await;
474 guard.write_bytes_at(offset, data).await.unwrap();
475 }));
476 }
477
478 for handle in handles {
479 handle.await.unwrap();
480 }
481
482 {
483 let mut guard = writer.lock().await;
484 guard.flush().await.unwrap();
485 }
486
487 let content = tokio::fs::read(&path).await.unwrap();
488 assert_eq!(content.len(), chunk_size * num_tasks);
489 for i in 0..num_tasks {
490 let start = i * chunk_size;
491 let expected = (i as u8) + 1;
492 let chunk = &content[start..start + chunk_size];
493 assert!(
494 chunk.iter().all(|&b| b == expected),
495 "data mismatch in task {} chunk",
496 i
497 );
498 }
499 }
500
501 #[tokio::test]
502 async fn test_concurrent_writes_separate_writers() {
503 let dir = tempfile::tempdir().unwrap();
507 let path = dir.path().join("test_concurrent_sep.bin");
508
509 let chunk_size: usize = 64 * 1024;
510 let num_tasks: usize = 4;
511
512 {
514 let mut w0 = PositionedDiskWriter::new(&path, Some((chunk_size * num_tasks) as u64));
515 w0.open().await.unwrap();
516 w0.flush().await.unwrap();
517 }
518
519 let mut handles = Vec::with_capacity(num_tasks);
520 for i in 0..num_tasks {
521 let offset = (i as u64) * chunk_size as u64;
522 let fill = (i as u8) + 1;
523 let data = vec![fill; chunk_size];
524 let path_clone = path.clone();
525 handles.push(tokio::spawn(async move {
526 let mut w = PositionedDiskWriter::new(&path_clone, None);
527 w.open().await.unwrap();
528 w.write_at(offset, &data).await.unwrap();
529 w.flush().await.unwrap();
530 }));
531 }
532
533 for handle in handles {
534 handle.await.unwrap();
535 }
536
537 let content = tokio::fs::read(&path).await.unwrap();
538 assert_eq!(content.len(), chunk_size * num_tasks);
539 for i in 0..num_tasks {
540 let start = i * chunk_size;
541 let expected = (i as u8) + 1;
542 let chunk = &content[start..start + chunk_size];
543 assert!(
544 chunk.iter().all(|&b| b == expected),
545 "data mismatch in separate-writer task {} chunk",
546 i
547 );
548 }
549 }
550
551 #[tokio::test]
552 async fn test_positioned_writer_write_bytes_at_zero_copy() {
553 let dir = tempfile::tempdir().unwrap();
554 let path = dir.path().join("test_zero_copy.bin");
555
556 let mut writer = PositionedDiskWriter::new(&path, Some(256));
557 writer.open().await.unwrap();
558
559 let data = bytes::Bytes::from(vec![0xAB; 128]);
560 writer.write_bytes_at(0, data).await.unwrap();
561 writer.flush().await.unwrap();
562
563 let mut buf = [0u8; 128];
564 let n = writer.read_at(0, &mut buf).await.unwrap();
565 assert_eq!(n, 128);
566 assert!(buf.iter().all(|&b| b == 0xAB));
567 }
568}
569
570#[cfg(all(target_os = "linux", feature = "io_uring"))]
586mod io_uring_backend {
587 use std::path::{Path, PathBuf};
588
589 use async_trait::async_trait;
590 use tracing::debug;
591
592 use crate::error::{Aria2Error, Result};
593
594 use super::SeekableDiskWriter;
595
596 pub struct IoUringDiskWriter {
616 file: Option<tokio_uring::fs::File>,
619 path: PathBuf,
620 total_size: Option<u64>,
621 }
622
623 impl IoUringDiskWriter {
624 pub fn new(path: &Path, total_size: Option<u64>) -> Self {
631 Self {
632 file: None,
633 path: path.to_path_buf(),
634 total_size,
635 }
636 }
637
638 pub fn total_size(&self) -> Option<u64> {
640 self.total_size
641 }
642 }
643
644 #[async_trait]
645 impl SeekableDiskWriter for IoUringDiskWriter {
646 async fn open(&mut self) -> Result<()> {
647 if self.file.is_some() {
648 return Ok(());
649 }
650
651 if let Some(parent) = self.path.parent()
655 && !parent.as_os_str().is_empty()
656 && !parent.exists()
657 {
658 std::fs::create_dir_all(parent)?;
659 debug!("Created parent directories for {:?}", self.path);
660 }
661
662 if let Some(size) = self.total_size {
670 let std_file = std::fs::OpenOptions::new()
671 .create(true)
672 .write(true)
673 .read(true)
674 .open(&self.path)?;
675 let current_size = std_file.metadata()?.len();
676 if current_size == 0 && size > 0 {
677 std_file.set_len(size)?;
678 debug!("Pre-allocated file to {} bytes: {:?}", size, self.path);
679 }
680 drop(std_file);
681 }
682
683 let file = tokio_uring::fs::OpenOptions::new()
686 .create(true)
687 .write(true)
688 .read(true)
689 .open(&self.path)
690 .await
691 .map_err(|e| Aria2Error::Io(format!("io_uring open failed: {e}")))?;
692 debug!("Opened file for io_uring positioned I/O: {:?}", self.path);
693
694 self.file = Some(file);
695 Ok(())
696 }
697
698 async fn write_at(&mut self, offset: u64, data: &[u8]) -> Result<()> {
699 let file = self
700 .file
701 .as_ref()
702 .ok_or_else(|| Aria2Error::Io("io_uring file not open".into()))?;
703 write_all_at_uring(file, data, offset).await
704 }
705
706 async fn write_bytes_at(&mut self, offset: u64, data: bytes::Bytes) -> Result<()> {
710 self.write_at(offset, &data).await
711 }
712
713 async fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize> {
714 let file = self
715 .file
716 .as_ref()
717 .ok_or_else(|| Aria2Error::Io("io_uring file not open".into()))?;
718 read_exact_at_uring(file, buf, offset).await
719 }
720
721 async fn truncate(&mut self, length: u64) -> Result<()> {
722 if let Some(file) = self.file.take() {
726 let _ = file.close().await.map_err(|e| {
727 Aria2Error::Io(format!("io_uring close during truncate failed: {e}"))
728 })?;
729 }
730 std::fs::OpenOptions::new()
731 .write(true)
732 .open(&self.path)?
733 .set_len(length)?;
734 let file = tokio_uring::fs::OpenOptions::new()
735 .create(true)
736 .write(true)
737 .read(true)
738 .open(&self.path)
739 .await
740 .map_err(|e| {
741 Aria2Error::Io(format!("io_uring reopen after truncate failed: {e}"))
742 })?;
743 self.file = Some(file);
744 Ok(())
745 }
746
747 async fn flush(&mut self) -> Result<()> {
748 if let Some(ref file) = self.file {
749 file.sync_all()
750 .await
751 .map_err(|e| Aria2Error::Io(format!("io_uring sync_all failed: {e}")))?;
752 }
753 Ok(())
754 }
755
756 async fn len(&self) -> Result<u64> {
757 if self.file.is_some() {
758 Ok(std::fs::metadata(&self.path)
762 .map(|m| m.len())
763 .unwrap_or_else(|_| self.total_size.unwrap_or(0)))
764 } else if let Some(size) = self.total_size {
765 Ok(size)
766 } else {
767 Ok(0)
768 }
769 }
770
771 fn path(&self) -> &Path {
772 &self.path
773 }
774
775 async fn close(&mut self) -> Result<()> {
777 if let Some(file) = self.file.take() {
778 file.close()
779 .await
780 .map_err(|e| Aria2Error::Io(format!("io_uring close failed: {e}")))?;
781 }
782 Ok(())
783 }
784 }
785
786 async fn write_all_at_uring(
793 file: &tokio_uring::fs::File,
794 mut buf: &[u8],
795 mut offset: u64,
796 ) -> Result<()> {
797 while !buf.is_empty() {
798 let (res, _) = file.write_at(buf, offset).await;
799 let n = res.map_err(|e| Aria2Error::Io(format!("io_uring write_at failed: {e}")))?;
800 if n == 0 {
801 return Err(Aria2Error::Io(
802 "io_uring write_at returned 0 — failed to write whole buffer".into(),
803 ));
804 }
805 offset += n as u64;
806 buf = &buf[n..];
807 }
808 Ok(())
809 }
810
811 async fn read_exact_at_uring(
815 file: &tokio_uring::fs::File,
816 mut buf: &mut [u8],
817 mut offset: u64,
818 ) -> Result<usize> {
819 let mut filled = 0usize;
820 while !buf.is_empty() {
821 let (res, returned_buf) = file.read_at(buf, offset).await;
822 let n = res.map_err(|e| Aria2Error::Io(format!("io_uring read_at failed: {e}")))?;
823 if n == 0 {
824 break; }
826 filled += n;
827 offset += n as u64;
828 buf = &mut returned_buf[n..];
829 }
830 Ok(filled)
831 }
832}
833
834#[cfg(all(target_os = "linux", feature = "io_uring"))]
836pub use io_uring_backend::IoUringDiskWriter;
837
838#[cfg(all(test, target_os = "linux", feature = "io_uring"))]
846mod io_uring_tests {
847 use super::IoUringDiskWriter;
848
849 #[test]
850 fn test_iouring_basic_write_read() {
851 tokio_uring::start(async {
852 let dir = tempfile::tempdir().unwrap();
853 let path = dir.path().join("iouring_basic.bin");
854
855 let mut writer = IoUringDiskWriter::new(&path, Some(1024));
856 writer.open().await.unwrap();
857 writer.write_at(0, b"hello io_uring").await.unwrap();
858 writer.flush().await.unwrap();
859
860 let mut buf = [0u8; 14];
861 let n = writer.read_at(0, &mut buf).await.unwrap();
862 assert_eq!(n, 14);
863 assert_eq!(&buf, b"hello io_uring");
864 });
865 }
866
867 #[test]
868 fn test_iouring_write_at_offset() {
869 tokio_uring::start(async {
870 let dir = tempfile::tempdir().unwrap();
871 let path = dir.path().join("iouring_offset.bin");
872
873 let mut writer = IoUringDiskWriter::new(&path, None);
874 writer.open().await.unwrap();
875 writer.write_at(100, b"offset data").await.unwrap();
876 writer.flush().await.unwrap();
877
878 let mut buf = [0u8; 11];
879 let n = writer.read_at(100, &mut buf).await.unwrap();
880 assert_eq!(n, 11);
881 assert_eq!(&buf, b"offset data");
882 });
883 }
884
885 #[test]
886 fn test_iouring_truncate_and_len() {
887 tokio_uring::start(async {
888 let dir = tempfile::tempdir().unwrap();
889 let path = dir.path().join("iouring_trunc.bin");
890
891 let mut writer = IoUringDiskWriter::new(&path, Some(2048));
892 writer.open().await.unwrap();
893
894 let len = writer.len().await.unwrap();
895 assert_eq!(len, 2048);
896
897 writer.truncate(512).await.unwrap();
898 let len = writer.len().await.unwrap();
899 assert_eq!(len, 512);
900 });
901 }
902
903 #[test]
904 fn test_iouring_close_reopen() {
905 tokio_uring::start(async {
906 let dir = tempfile::tempdir().unwrap();
907 let path = dir.path().join("iouring_close.bin");
908
909 let mut writer = IoUringDiskWriter::new(&path, None);
910 writer.open().await.unwrap();
911 writer.write_at(0, b"before close").await.unwrap();
912 writer.flush().await.unwrap();
913 writer.close().await.unwrap();
914
915 writer.open().await.unwrap();
916 writer.write_at(12, b" after reopen").await.unwrap();
917 writer.flush().await.unwrap();
918 writer.close().await.unwrap();
919
920 let content = std::fs::read(&path).unwrap();
921 assert_eq!(&content, b"before close after reopen");
922 });
923 }
924
925 #[test]
926 fn test_iouring_resume_does_not_truncate() {
927 tokio_uring::start(async {
928 let dir = tempfile::tempdir().unwrap();
929 let path = dir.path().join("iouring_resume.bin");
930
931 {
933 let mut w = IoUringDiskWriter::new(&path, Some(1024));
934 w.open().await.unwrap();
935 w.write_at(0, b"resume-data").await.unwrap();
936 w.flush().await.unwrap();
937 w.close().await.unwrap();
938 }
939
940 {
942 let mut w = IoUringDiskWriter::new(&path, Some(1024));
943 w.open().await.unwrap();
944 let mut buf = [0u8; 11];
945 let n = w.read_at(0, &mut buf).await.unwrap();
946 assert_eq!(n, 11);
947 assert_eq!(&buf, b"resume-data", "existing data must survive reopen");
948 }
949 });
950 }
951
952 #[test]
953 fn test_iouring_creates_parent_dirs() {
954 tokio_uring::start(async {
955 let dir = tempfile::tempdir().unwrap();
956 let path = dir.path().join("nested").join("deep").join("file.bin");
957
958 let mut writer = IoUringDiskWriter::new(&path, Some(64));
959 writer.open().await.unwrap();
960 writer.write_at(0, b"x").await.unwrap();
961 writer.flush().await.unwrap();
962
963 assert!(path.exists(), "file should be created with parent dirs");
964 });
965 }
966
967 #[test]
968 fn test_iouring_write_bytes_at() {
969 tokio_uring::start(async {
970 let dir = tempfile::tempdir().unwrap();
971 let path = dir.path().join("iouring_bytes.bin");
972
973 let mut writer = IoUringDiskWriter::new(&path, Some(256));
974 writer.open().await.unwrap();
975
976 let data = bytes::Bytes::from(vec![0xAB; 128]);
977 writer.write_bytes_at(0, data).await.unwrap();
978 writer.flush().await.unwrap();
979
980 let mut buf = [0u8; 128];
981 let n = writer.read_at(0, &mut buf).await.unwrap();
982 assert_eq!(n, 128);
983 assert!(buf.iter().all(|&b| b == 0xAB));
984 });
985 }
986}