use std::fs;
use std::io;
use std::path::Path;
pub(crate) const BATCH: usize = 64;
pub(crate) struct BatchFileReader {
#[cfg(all(target_os = "linux", feature = "io-uring"))]
ring: Option<ring_backend::RingReader>,
}
impl BatchFileReader {
pub(crate) fn new() -> Self {
#[cfg(all(target_os = "linux", feature = "io-uring"))]
{
Self { ring: ring_backend::RingReader::new(BATCH).ok() }
}
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
{
Self {}
}
}
pub(crate) fn read_batch(&mut self, paths: &[&Path]) -> Vec<io::Result<String>> {
#[cfg(all(target_os = "linux", feature = "io-uring"))]
if let Some(r) = self.ring.as_mut() {
return r.read_batch(paths);
}
paths.iter().map(fs::read_to_string).collect()
}
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
mod ring_backend {
use io_uring::{opcode, types, IoUring};
use std::ffi::CString;
use std::io;
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
const AT_FDCWD: i32 = -100;
const INITIAL_CAP: usize = 64 * 1024;
struct Slot {
fd: RawFd,
err: Option<io::Error>,
buf: Vec<u8>,
len: usize,
done: bool,
}
impl Slot {
fn fresh() -> Self {
Self { fd: -1, err: None, buf: Vec::with_capacity(INITIAL_CAP), len: 0, done: false }
}
fn take_fd(&mut self) -> Option<OwnedFd> {
if self.fd >= 0 {
let owned = unsafe { OwnedFd::from_raw_fd(self.fd) };
self.fd = -1;
Some(owned)
} else {
None
}
}
}
pub(crate) struct RingReader {
ring: IoUring,
depth: usize,
slots: Vec<Slot>,
broken: bool,
pending_paths: Vec<CString>,
}
impl RingReader {
pub(crate) fn new(depth: usize) -> io::Result<Self> {
let depth = depth.max(1);
let ring = IoUring::new(depth as u32)?;
let slots = (0..depth).map(|_| Slot::fresh()).collect();
Ok(Self { ring, depth, slots, broken: false, pending_paths: Vec::new() })
}
pub(crate) fn read_batch(&mut self, paths: &[&Path]) -> Vec<io::Result<String>> {
if self.broken {
return paths.iter().map(|p| std::fs::read_to_string(p)).collect();
}
match self.try_read_batch(paths) {
Ok(v) => v,
Err(_) => paths.iter().map(|p| std::fs::read_to_string(p)).collect(),
}
}
fn try_read_batch(&mut self, paths: &[&Path]) -> io::Result<Vec<io::Result<String>>> {
let mut out: Vec<io::Result<String>> = Vec::with_capacity(paths.len());
for chunk in paths.chunks(self.depth) {
out.extend(self.read_chunk(chunk)?);
}
Ok(out)
}
fn read_chunk(&mut self, paths: &[&Path]) -> io::Result<Vec<io::Result<String>>> {
let n = paths.len();
let mut mach_err: Option<io::Error> = None;
let mut cpaths: Vec<Option<CString>> = Vec::with_capacity(n);
for p in paths {
cpaths.push(CString::new(p.as_os_str().as_bytes()).ok());
}
if self.slots.len() < n {
self.slots.extend((self.slots.len()..n).map(|_| Slot::fresh()));
}
for i in 0..n {
let s = &mut self.slots[i];
s.fd = -1;
s.len = 0;
s.done = cpaths[i].is_none();
s.err = if s.done {
Some(io::Error::new(
io::ErrorKind::InvalidInput,
"文件名包含非法 NUL 字节",
))
} else {
None
};
if s.buf.capacity() < INITIAL_CAP {
s.buf = Vec::with_capacity(INITIAL_CAP);
}
s.buf.clear();
}
let mut submitted = 0usize;
{
let RingReader { ring, .. } = self;
let mut sq = ring.submission();
for (i, c) in cpaths.iter().enumerate() {
let Some(c) = c else { continue };
let e = opcode::OpenAt::new(types::Fd(AT_FDCWD), c.as_ptr())
.build()
.user_data(i as u64);
if unsafe { sq.push(&e) }.is_err() {
mach_err = Some(io::Error::other("io_uring SQ 满"));
break;
}
submitted += 1;
}
}
if submitted > 0 {
match self.ring.submit_and_wait(submitted) {
Err(e) => mach_err = Some(e),
Ok(_) => {
for c in self.ring.completion() {
let s = &mut self.slots[c.user_data() as usize];
if c.result() < 0 {
s.err = Some(io::Error::from_raw_os_error(-c.result()));
s.done = true;
} else {
s.fd = c.result();
}
}
}
}
}
while mach_err.is_none() {
let mut inflight = 0usize;
{
let RingReader { ring, slots, .. } = self;
let mut sq = ring.submission();
for (i, s) in slots.iter_mut().enumerate().take(n) {
if s.done || s.fd < 0 {
continue;
}
let cap = s.buf.capacity().max(INITIAL_CAP);
if s.buf.len() < cap {
s.buf.reserve(cap - s.buf.len());
unsafe { s.buf.set_len(cap) };
}
let e = opcode::Read::new(
types::Fd(s.fd),
unsafe { s.buf.as_mut_ptr().add(s.len) },
(s.buf.len() - s.len) as u32,
)
.offset(s.len as u64)
.build()
.user_data(i as u64);
if unsafe { sq.push(&e) }.is_err() {
mach_err = Some(io::Error::other("io_uring SQ 满"));
break;
}
inflight += 1;
}
}
if inflight == 0 || mach_err.is_some() {
break;
}
if let Err(e) = self.ring.submit_and_wait(inflight) {
mach_err = Some(e);
break;
}
let mut need_grow: Vec<usize> = Vec::new();
for c in self.ring.completion() {
let i = c.user_data() as usize;
let res = c.result();
let s = &mut self.slots[i];
if res < 0 {
s.err = Some(io::Error::from_raw_os_error(-res));
s.done = true;
} else if s.len + res as usize == s.buf.len() {
s.len += res as usize;
need_grow.push(i);
} else {
s.len += res as usize;
s.buf.truncate(s.len);
s.done = true;
}
}
for i in need_grow {
let s = &mut self.slots[i];
let new_cap = s.buf.capacity() * 2;
s.buf.reserve(new_cap - s.buf.len());
}
}
if mach_err.is_none() {
let mut closing = 0usize;
let mut push_failed = false;
{
let RingReader { ring, slots, .. } = self;
let mut sq = ring.submission();
for s in slots.iter_mut().take(n) {
if s.fd >= 0 {
let e = opcode::Close::new(types::Fd(s.fd)).build().user_data(0);
if unsafe { sq.push(&e) }.is_err() {
push_failed = true;
break;
}
closing += 1;
}
}
}
if push_failed {
mach_err = Some(io::Error::other("io_uring SQ 满"));
} else if closing > 0 {
match self.ring.submit_and_wait(closing) {
Ok(_) => {
for _ in self.ring.completion() {}
for s in self.slots.iter_mut().take(n) {
s.fd = -1; }
}
Err(e) => {
self.mark_broken(&mut cpaths, &e);
return Err(e);
}
}
}
}
if mach_err.is_some() {
let e = mach_err.take().unwrap();
self.mark_broken(&mut cpaths, &e);
for s in self.slots.iter_mut().take(n) {
drop(s.take_fd());
}
return Err(e);
}
let mut results = Vec::with_capacity(n);
for i in 0..n {
let s = &mut self.slots[i];
if let Some(e) = s.err.take() {
s.buf.clear();
results.push(Err(e));
continue;
}
debug_assert!(s.done);
let buf = std::mem::take(&mut s.buf);
s.len = 0;
s.done = false;
results.push(match String::from_utf8(buf) {
Ok(text) => Ok(text),
Err(_) => Err(io::Error::new(
io::ErrorKind::InvalidData,
"stream did not contain valid UTF-8",
)),
});
}
Ok(results)
}
fn mark_broken(&mut self, cpaths: &mut Vec<Option<CString>>, _e: &io::Error) {
self.broken = true;
self.pending_paths
.extend(cpaths.drain(..).flatten());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_files(count: usize) -> (tempfile::TempDir, Vec<std::path::PathBuf>) {
let dir = tempfile::tempdir().unwrap();
let mut paths = Vec::new();
for i in 0..count {
let p = dir.path().join(format!("f{i}.java"));
std::fs::write(&p, format!("class A{i} {{}}\n")).unwrap();
paths.push(p);
}
(dir, paths)
}
#[test]
fn ring_matches_std_read() {
let (dir, paths) = tmp_files(100);
let refs: Vec<&Path> = paths.iter().map(|p| p.as_path()).collect();
let mut r = RingReader::new(16).unwrap();
for chunk in refs.chunks(7) {
let got = r.read_batch(chunk);
for (g, p) in got.iter().zip(chunk) {
assert_eq!(g.as_ref().unwrap(), &std::fs::read_to_string(p).unwrap());
}
}
drop(dir);
}
#[test]
fn ring_errors_align_with_paths() {
let (dir, paths) = tmp_files(4);
let mut mixed: Vec<&Path> = paths.iter().map(|p| p.as_path()).collect();
mixed.push(Path::new("/nonexistent-definitely-missing/xx.java"));
let mut r = RingReader::new(16).unwrap();
let got = r.read_batch(&mixed);
assert_eq!(got.len(), mixed.len());
for (g, p) in got.iter().zip(&mixed) {
let std_res = std::fs::read_to_string(p);
assert_eq!(g.is_ok(), std_res.is_ok(), "path={:?}", p);
}
drop(dir);
}
#[test]
fn ring_oversize_buffer_growth() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("big.java");
let body: String = "class Big { int x; }\n".repeat(11_000);
std::fs::write(&p, &body).unwrap();
let mut r = RingReader::new(16).unwrap();
let got = r.read_batch(&[p.as_path()]);
assert_eq!(got[0].as_ref().unwrap(), &body);
}
#[test]
fn ring_empty_and_non_utf8() {
let dir = tempfile::tempdir().unwrap();
let empty = dir.path().join("empty.java");
std::fs::write(&empty, b"").unwrap();
let latin = dir.path().join("latin.java");
std::fs::write(&latin, b"class A { /* \xff\xfe */ }").unwrap();
let mut r = RingReader::new(16).unwrap();
let got = r.read_batch(&[empty.as_path(), latin.as_path()]);
assert_eq!(got[0].as_ref().unwrap(), "");
assert!(got[1].is_err());
}
#[test]
fn ring_exact_buffer_multiple() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("exact.java");
let body = vec![b'a'; INITIAL_CAP];
std::fs::write(&p, &body).unwrap();
let mut r = RingReader::new(16).unwrap();
let got = r.read_batch(&[p.as_path()]);
assert_eq!(got[0].as_ref().unwrap().as_bytes(), &body[..]);
}
}
}