use {
crate::{FileSize, io_setup::IoSetupState},
std::{fs, io, path::Path},
};
#[cfg(target_os = "linux")]
const DEFAULT_IO_URING_BUFFER_SIZE: usize =
4 * crate::io_uring::file_creator::DEFAULT_WRITE_SIZE as usize;
#[cfg(not(target_os = "linux"))]
const DEFAULT_BUF_WRITER_BUFFER_SIZE: usize = 2 * 1024 * 1024;
pub fn large_file_buf_writer(
path: impl AsRef<Path>,
io_setup: &IoSetupState,
) -> io::Result<impl io::Write> {
#[cfg(target_os = "linux")]
{
assert!(agave_io_uring::io_uring_supported());
use {
crate::io_uring::{
file_creator::IoUringFileCreatorBuilder, file_writer::IoUringFileWriter,
},
std::sync::Arc,
};
let file_creator = IoUringFileCreatorBuilder::new()
.use_registered_buffers(io_setup.use_registered_io_uring_buffers)
.write_with_direct_io(io_setup.use_direct_io)
.shared_sqpoll(io_setup.shared_sqpoll_fd())
.build(DEFAULT_IO_URING_BUFFER_SIZE, |_| None)?;
IoUringFileWriter::new(
file_creator,
path.as_ref().to_path_buf(),
0o666,
Arc::new(fs::File::open(path.as_ref().parent().ok_or(
io::Error::new(
io::ErrorKind::InvalidInput,
"expected path to an output file that has a parent directory",
),
)?)?),
)
}
#[cfg(not(target_os = "linux"))]
{
let _ = io_setup;
let file = fs::File::create(path)?;
Ok(io::BufWriter::with_capacity(
DEFAULT_BUF_WRITER_BUFFER_SIZE,
file,
))
}
}
pub struct SizeLimitedWriter<W: io::Write> {
inner: W,
limit: FileSize,
bytes_written: FileSize,
}
impl<W: io::Write> SizeLimitedWriter<W> {
pub fn new(inner: W, limit: FileSize) -> Self {
Self {
inner,
limit,
bytes_written: 0,
}
}
pub fn bytes_written(&self) -> FileSize {
self.bytes_written
}
}
impl<W: io::Write> io::Write for SizeLimitedWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let attempted_size = self.bytes_written.checked_add(buf.len() as FileSize);
if attempted_size.is_none_or(|size| size > self.limit) {
return Err(io::Error::new(
io::ErrorKind::FileTooLarge,
format!(
"write of {} bytes would exceed limit of {} ({} already written)",
buf.len(),
self.limit,
self.bytes_written,
),
));
}
let n = self.inner.write(buf)?;
self.bytes_written = self.bytes_written.wrapping_add(n as FileSize);
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
#[cfg(test)]
mod tests {
use {super::*, io::Write as _, test_case::test_case};
#[test_case(10, b"hello" ; "within limit")]
#[test_case(5, b"hello" ; "exactly at limit")]
fn size_limited_writer_accepts(limit: FileSize, data: &[u8]) {
let mut buf = Vec::new();
let mut w = SizeLimitedWriter::new(&mut buf, limit);
w.write_all(data).unwrap();
assert_eq!(w.bytes_written(), data.len() as FileSize);
assert_eq!(buf, data);
}
#[test_case(4, b"hello" ; "exceeds limit")]
#[test_case(0, b"x" ; "zero limit")]
fn size_limited_writer_rejects(limit: FileSize, data: &[u8]) {
let mut buf = Vec::new();
let mut w = SizeLimitedWriter::new(&mut buf, limit);
let err = w.write(data).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::FileTooLarge);
assert_eq!(w.bytes_written(), 0);
assert!(buf.is_empty());
}
#[test]
fn size_limited_writer_rejects_on_cumulative_overflow() {
let mut buf = Vec::new();
let mut w = SizeLimitedWriter::new(&mut buf, 5);
w.write_all(b"hi").unwrap();
let err = w.write(b"four").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::FileTooLarge);
assert_eq!(w.bytes_written(), 2);
assert_eq!(buf, b"hi");
}
}