Skip to main content

vfsi_core/api/
read.rs

1use super::support::FsReadResult as OwnedReadResult;
2use super::*;
3use std::path::Path;
4
5/// One consuming read operation. Construction performs no I/O or allocation.
6/// Caller buffers are borrowed only until `Vfsi::vread` returns, including on errors.
7#[must_use = "configuration and requests have no effect until used"]
8pub struct ReadOp<'a, H: crate::api::FileHandle + 'a> {
9    source: OpSource<'a, H>,
10}
11enum OpSource<'a, H: crate::api::FileHandle + 'a> {
12    Whole(&'a Path),
13    Range(&'a H, u64, usize),
14    Into(&'a H, u64, &'a mut [u8]),
15}
16impl<H: crate::api::FileHandle> std::fmt::Debug for ReadOp<'_, H> {
17    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match &self.source {
19            OpSource::Whole(path) => formatter.debug_tuple("ReadOp::whole").field(path).finish(),
20            OpSource::Range(_, _, length) => formatter
21                .debug_struct("ReadOp::range")
22                .field("length", length)
23                .finish_non_exhaustive(),
24            OpSource::Into(_, _, buffer) => formatter
25                .debug_struct("ReadOp::into")
26                .field("length", &buffer.len())
27                .finish_non_exhaustive(),
28        }
29    }
30}
31impl<'a, H: crate::api::FileHandle + 'a> From<&'a str> for ReadOp<'a, H> {
32    fn from(path: &'a str) -> Self {
33        Self::whole(path)
34    }
35}
36impl<'a, H: crate::api::FileHandle + 'a> From<&'a Path> for ReadOp<'a, H> {
37    fn from(path: &'a Path) -> Self {
38        Self::whole(path)
39    }
40}
41impl<'a, H: crate::api::FileHandle + 'a> ReadOp<'a, H> {
42    /// Collect a complete file; fail instead of truncating at the byte budget.
43    pub fn whole<P: AsRef<Path> + ?Sized>(path: &'a P) -> Self {
44        Self {
45            source: OpSource::Whole(path.as_ref()),
46        }
47    }
48    /// Allocate a positional range. Short progress and EOF are reported explicitly.
49    pub fn range(file: &'a H, offset: u64, length: usize) -> Self {
50        Self {
51            source: OpSource::Range(file, offset, length),
52        }
53    }
54    /// Fill caller storage at an absolute offset, without changing the cursor.
55    /// The request length is exactly `buffer.len()`.
56    pub fn into(file: &'a H, offset: u64, buffer: &'a mut [u8]) -> Self {
57        Self {
58            source: OpSource::Into(file, offset, buffer),
59        }
60    }
61    /// Inspect a complete-file path from an external `Vfsi` implementation.
62    pub fn whole_file_path(&self) -> Option<&'a Path> {
63        match &self.source {
64            OpSource::Whole(path) => Some(*path),
65            _ => None,
66        }
67    }
68    /// Inspect the handle, absolute offset, and length of an allocating range.
69    pub fn range_parts(&self) -> Option<(&'a H, u64, usize)> {
70        match &self.source {
71            OpSource::Range(file, offset, length) => Some((*file, *offset, *length)),
72            _ => None,
73        }
74    }
75    /// Borrow the handle, absolute offset, and caller storage for direct dispatch.
76    pub fn buffer_parts_mut(&mut self) -> Option<(&'a H, u64, &mut [u8])> {
77        match &mut self.source {
78            OpSource::Into(file, offset, buffer) => Some((*file, *offset, buffer)),
79            _ => None,
80        }
81    }
82}
83
84/// Progress for one operation, with no borrowed references to caller storage.
85#[derive(Clone, PartialEq, Eq)]
86pub struct ReadResult {
87    offset: u64,
88    eof: bool,
89    payload: ReadPayload,
90}
91#[derive(Clone, PartialEq, Eq)]
92enum ReadPayload {
93    Owned(Vec<u8>),
94    Buffered(usize),
95}
96impl std::fmt::Debug for ReadResult {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.debug_struct("ReadResult")
99            .field("offset", &self.offset())
100            .field("bytes_read", &self.bytes_read())
101            .field("eof", &self.eof())
102            .field("data", &self.data())
103            .finish()
104    }
105}
106impl ReadResult {
107    /// Owned contents. The byte count is derived, never independently supplied.
108    pub fn owned(offset: u64, data: Vec<u8>, eof: bool) -> Self {
109        Self {
110            offset,
111            eof,
112            payload: ReadPayload::Owned(data),
113        }
114    }
115    /// Progress in caller storage, for implementing Vfsi. The implementation
116    /// must keep bytes_read within the corresponding destination length.
117    pub const fn buffered(offset: u64, bytes_read: usize, eof: bool) -> Self {
118        Self {
119            offset,
120            eof,
121            payload: ReadPayload::Buffered(bytes_read),
122        }
123    }
124    pub const fn offset(&self) -> u64 {
125        self.offset
126    }
127    /// Number of valid bytes in owned data or caller-provided storage.
128    pub const fn bytes_read(&self) -> usize {
129        match &self.payload {
130            ReadPayload::Owned(data) => data.len(),
131            ReadPayload::Buffered(count) => *count,
132        }
133    }
134    pub const fn eof(&self) -> bool {
135        self.eof
136    }
137    pub fn data(&self) -> Option<&[u8]> {
138        match &self.payload {
139            ReadPayload::Owned(data) => Some(data.as_slice()),
140            ReadPayload::Buffered(_) => None,
141        }
142    }
143    pub fn into_data(self) -> Option<Vec<u8>> {
144        match self.payload {
145            ReadPayload::Owned(data) => Some(data),
146            ReadPayload::Buffered(_) => None,
147        }
148    }
149    pub const fn is_buffered(&self) -> bool {
150        matches!(&self.payload, ReadPayload::Buffered(_))
151    }
152}
153
154/// Fluent configuration must be retained; discarding a changed copy is diagnosed.
155///
156/// ```compile_fail
157/// #![deny(unused_must_use)]
158/// use vfsi_core::api::ReadOptions;
159/// let options = ReadOptions::new();
160/// options.with_max_total_bytes(std::num::NonZeroUsize::new(4096).unwrap());
161/// ```
162///
163/// Aggregate logical byte budget, defaulting to the client's policy (16 MiB).
164/// Range request lengths must fit even if EOF returns fewer bytes. Complete
165/// files share the remaining returned-data budget and fail rather than truncate.
166///
167/// ```
168/// use std::num::NonZeroUsize;
169/// use vfsi_core::api::ReadOptions;
170/// let inherited = ReadOptions::new();
171/// let limited = inherited.with_max_total_bytes(NonZeroUsize::new(4096).unwrap());
172/// assert_eq!(limited.max_total_bytes(), NonZeroUsize::new(4096));
173/// assert_eq!(limited.max_total_bytes_or(8192), 4096);
174/// assert_eq!(inherited.max_total_bytes(), None);
175/// assert_eq!(inherited.max_total_bytes_or(8192), 8192);
176/// ```
177/// Overrides require an explicitly validated nonzero value.
178///
179/// ```compile_fail
180/// use vfsi_core::api::ReadOptions;
181/// ReadOptions::new().with_max_total_bytes(std::num::NonZeroUsize::new(0));
182/// ```
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184#[must_use = "configuration and requests have no effect until used"]
185pub struct ReadOptions {
186    max_total_bytes: Option<std::num::NonZeroUsize>,
187}
188impl ReadOptions {
189    /// Inherit the client's aggregate read budget.
190    pub const fn new() -> Self {
191        Self {
192            max_total_bytes: None,
193        }
194    }
195    /// Override the aggregate budget for this call. New options inherit client policy.
196    /// Only nonzero overrides are representable. Stream larger files instead
197    /// of raising this when bounded memory is required.
198    pub const fn with_max_total_bytes(mut self, bytes: std::num::NonZeroUsize) -> Self {
199        self.max_total_bytes = Some(bytes);
200        self
201    }
202    /// Return the configured override, not the effective client budget.
203    /// `None` inherits the client's budget; it does not mean unlimited.
204    pub const fn max_total_bytes(self) -> Option<std::num::NonZeroUsize> {
205        self.max_total_bytes
206    }
207    /// Resolve an inherited aggregate budget against the client's default.
208    pub fn max_total_bytes_or(self, default: usize) -> usize {
209        self.max_total_bytes
210            .map_or(default, std::num::NonZeroUsize::get)
211    }
212}
213
214impl Default for ReadOptions {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219pub fn consume_ops<'a, H: crate::api::FileHandle + 'a, R, B>(
220    ops: impl IntoIterator<Item = ReadOp<'a, H>>,
221    budget: usize,
222    range: impl Fn(&'a H, u64, usize) -> R,
223    into: impl Fn(&'a H, u64, &'a mut [u8]) -> B,
224    owned: impl FnOnce(&[ReadRequest<'a, R>], usize) -> Result<Vec<OwnedReadResult>>,
225    buffers: impl FnOnce(&mut [B], usize) -> Result<Vec<crate::api::ReadIntoResult>>,
226) -> Result<Vec<ReadResult>> {
227    let mut ops = ops.into_iter();
228    let Some(first) = ops.next() else {
229        return Ok(Vec::new());
230    };
231    let second = ops.next();
232    if second.is_none() {
233        let length = match &first.source {
234            OpSource::Whole(_) => 0,
235            OpSource::Range(_, _, length) => *length,
236            OpSource::Into(_, _, buffer) => buffer.len(),
237        };
238        if length > budget {
239            return Err(Error::client(0, libc::EFBIG as u32));
240        }
241        let remap = |error: Error| match error.index() {
242            Some(0) | None => error,
243            Some(_) => Error::transport(None, "invalid vread_native backend error index"),
244        };
245        let request = match first.source {
246            OpSource::Whole(path) => ReadRequest::whole_file(path),
247            OpSource::Range(file, offset, length) => {
248                ReadRequest::range(range(file, offset, length))
249            }
250            OpSource::Into(file, offset, buffer) => {
251                let values = buffers(&mut [into(file, offset, buffer)], budget).map_err(remap)?;
252                if values.len() != 1 {
253                    return Err(Error::transport(
254                        None,
255                        "invalid vread_native buffer result count",
256                    ));
257                }
258                if values[0].bytes_read > budget {
259                    return Err(Error::transport(
260                        None,
261                        "vread_native backend exceeded byte budget",
262                    ));
263                }
264                return Ok(values
265                    .into_iter()
266                    .map(|result| {
267                        ReadResult::buffered(result.offset, result.bytes_read, result.eof)
268                    })
269                    .collect());
270            }
271        };
272        let values = owned(&[request], budget).map_err(remap)?;
273        if values.len() != 1 {
274            return Err(Error::transport(
275                None,
276                "invalid vread_native owned result count",
277            ));
278        }
279        if values[0].data.len() > budget {
280            return Err(Error::transport(
281                None,
282                "vread_native backend exceeded byte budget",
283            ));
284        }
285        return Ok(values
286            .into_iter()
287            .map(|result| ReadResult::owned(result.offset, result.data, result.eof))
288            .collect());
289    }
290    let ops = std::iter::once(first).chain(second).chain(ops);
291    let mut owned_ops = Vec::new();
292    let mut owned_indices = Vec::new();
293    let mut buffer_ops = Vec::new();
294    let mut buffer_indices = Vec::new();
295    let mut requested = 0usize;
296    let mut count = 0usize;
297    for (index, op) in ops.enumerate() {
298        count += 1;
299        let length = match &op.source {
300            OpSource::Whole(_) => 0,
301            OpSource::Range(_, _, length) => *length,
302            OpSource::Into(_, _, buffer) => buffer.len(),
303        };
304        requested = requested
305            .checked_add(length)
306            .filter(|bytes| *bytes <= budget)
307            .ok_or_else(|| Error::client(index, libc::EFBIG as u32))?;
308        match op.source {
309            OpSource::Whole(path) => {
310                owned_ops.push(ReadRequest::whole_file(path));
311                owned_indices.push(index);
312            }
313            OpSource::Range(file, offset, length) => {
314                owned_ops.push(ReadRequest::range(range(file, offset, length)));
315                owned_indices.push(index);
316            }
317            OpSource::Into(file, offset, buffer) => {
318                buffer_ops.push(into(file, offset, buffer));
319                buffer_indices.push(index);
320            }
321        }
322    }
323    let remap = |error: Error, indices: &[usize]| match error.index() {
324        Some(index) if index < indices.len() => error.with_index(indices[index]),
325        Some(_) => Error::transport(None, "invalid vread_native backend error index"),
326        None => error,
327    };
328    let buffered = if buffer_ops.is_empty() {
329        Vec::new()
330    } else {
331        buffers(&mut buffer_ops, budget).map_err(|e| remap(e, &buffer_indices))?
332    };
333    drop(buffer_ops);
334    if buffered.len() != buffer_indices.len() {
335        return Err(Error::transport(
336            None,
337            "invalid vread_native buffer result count",
338        ));
339    }
340    let mut remaining = budget;
341    for result in &buffered {
342        remaining = remaining
343            .checked_sub(result.bytes_read)
344            .ok_or_else(|| Error::transport(None, "vread_native backend exceeded byte budget"))?;
345    }
346    let allocated = if owned_ops.is_empty() {
347        Vec::new()
348    } else {
349        owned(&owned_ops, remaining).map_err(|e| remap(e, &owned_indices))?
350    };
351    if allocated.len() != owned_indices.len() {
352        return Err(Error::transport(
353            None,
354            "invalid vread_native owned result count",
355        ));
356    }
357    for result in &allocated {
358        remaining = remaining
359            .checked_sub(result.data.len())
360            .ok_or_else(|| Error::transport(None, "vread_native backend exceeded byte budget"))?;
361    }
362    if buffer_indices.is_empty() {
363        return Ok(allocated
364            .into_iter()
365            .map(|result| ReadResult::owned(result.offset, result.data, result.eof))
366            .collect());
367    }
368    if owned_indices.is_empty() {
369        return Ok(buffered
370            .into_iter()
371            .map(|result| ReadResult::buffered(result.offset, result.bytes_read, result.eof))
372            .collect());
373    }
374    let mut output: Vec<Option<ReadResult>> = (0..count).map(|_| None).collect();
375    for (index, result) in buffer_indices.into_iter().zip(buffered) {
376        output[index] = Some(ReadResult::buffered(
377            result.offset,
378            result.bytes_read,
379            result.eof,
380        ));
381    }
382    for (index, result) in owned_indices.into_iter().zip(allocated) {
383        output[index] = Some(ReadResult::owned(result.offset, result.data, result.eof));
384    }
385    output
386        .into_iter()
387        .map(|result| result.ok_or_else(|| Error::transport(None, "vread_native omitted a result")))
388        .collect()
389}
390/// Internal projection for the native owned-result batch path.
391pub struct ReadRequest<'a, R> {
392    pub(crate) source: Source<'a, R>,
393}
394pub(crate) enum Source<'a, R> {
395    Whole(&'a Path),
396    Range(R),
397}
398impl<'a, R> ReadRequest<'a, R> {
399    /// Read a complete path within the aggregate budget, without manual OPEN/CLOSE.
400    pub fn whole_file<P: AsRef<Path> + ?Sized>(path: &'a P) -> Self {
401        Self {
402            source: Source::Whole(path.as_ref()),
403        }
404    }
405    /// Wrap a positional request, preserving short results and EOF.
406    pub fn range(request: R) -> Self {
407        Self {
408            source: Source::Range(request),
409        }
410    }
411    /// Borrow the backend-defined range request, or return `None` for a whole file.
412    /// External [`crate::api::Vfsi`] implementations can use this together with
413    /// a whole-file dispatch path without exposing handles.
414    pub fn range_ref(&self) -> Option<&R> {
415        match &self.source {
416            Source::Range(request) => Some(request),
417            _ => None,
418        }
419    }
420}
421impl<'a, R> From<R> for ReadRequest<'a, R> {
422    fn from(request: R) -> Self {
423        Self::range(request)
424    }
425}
426pub fn read_batch<R>(
427    requests: &[ReadRequest<'_, R>],
428    budget: usize,
429    read_ranges: impl FnOnce(&[&R], usize) -> Result<Vec<OwnedReadResult>>,
430    read_paths: impl FnOnce(&[&Path], usize) -> Result<Vec<Vec<u8>>>,
431) -> Result<Vec<OwnedReadResult>> {
432    if requests.is_empty() {
433        return Ok(Vec::new());
434    }
435    let (mut paths, mut path_indices, mut ranges, mut range_indices) =
436        (Vec::new(), Vec::new(), Vec::new(), Vec::new());
437    for (i, r) in requests.iter().enumerate() {
438        match &r.source {
439            Source::Whole(path) => {
440                paths.push(*path);
441                path_indices.push(i);
442            }
443            Source::Range(range) => {
444                ranges.push(range);
445                range_indices.push(i);
446            }
447        }
448    }
449    let remap = |e: Error, indices: &[usize]| match e.index() {
450        Some(i) if i < indices.len() => e.with_index(indices[i]),
451        Some(_) => Error::transport(None, "invalid vread_native backend error index"),
452        None => e,
453    };
454    // Group ranges before complete paths. Reads do not promise a snapshot.
455    let range_results = if ranges.is_empty() {
456        Vec::new()
457    } else {
458        read_ranges(&ranges, budget).map_err(|e| remap(e, &range_indices))?
459    };
460    if range_results.len() != ranges.len() {
461        return Err(Error::transport(
462            None,
463            "invalid vread_native range result count",
464        ));
465    }
466    let mut remaining = budget;
467    for r in &range_results {
468        remaining = remaining
469            .checked_sub(r.data.len())
470            .ok_or_else(|| Error::transport(None, "vread_native backend exceeded byte budget"))?;
471    }
472    let path_results = if paths.is_empty() {
473        Vec::new()
474    } else {
475        read_paths(&paths, remaining).map_err(|e| remap(e, &path_indices))?
476    };
477    if path_results.len() != paths.len() {
478        return Err(Error::transport(
479            None,
480            "invalid vread_native path result count",
481        ));
482    }
483    for r in &path_results {
484        remaining = remaining
485            .checked_sub(r.len())
486            .ok_or_else(|| Error::transport(None, "vread_native backend exceeded byte budget"))?;
487    }
488    if path_indices.is_empty() {
489        return Ok(range_results);
490    }
491    if range_indices.is_empty() {
492        return Ok(path_results
493            .into_iter()
494            .map(|data| OwnedReadResult {
495                offset: 0,
496                data,
497                eof: true,
498            })
499            .collect());
500    }
501    let mut output: Vec<Option<OwnedReadResult>> = (0..requests.len()).map(|_| None).collect();
502    for (i, r) in range_indices.into_iter().zip(range_results) {
503        output[i] = Some(r);
504    }
505    for (i, data) in path_indices.into_iter().zip(path_results) {
506        output[i] = Some(OwnedReadResult {
507            offset: 0,
508            data,
509            eof: true,
510        });
511    }
512    output
513        .into_iter()
514        .map(|r| r.ok_or_else(|| Error::transport(None, "vread_native omitted a result")))
515        .collect()
516}
517
518#[cfg(test)]
519mod option_layout_tests {
520    use super::ReadOptions;
521    use std::num::NonZeroUsize;
522    #[test]
523    fn read_options_are_one_word_and_keep_inheritance_distinct() {
524        assert_eq!(
525            std::mem::size_of::<ReadOptions>(),
526            std::mem::size_of::<usize>()
527        );
528        let mut options = ReadOptions::new();
529        assert_eq!(options.max_total_bytes_or(7), 7);
530        for bytes in [1, 42, usize::MAX] {
531            let limit = NonZeroUsize::new(bytes).unwrap();
532            options = options.with_max_total_bytes(limit);
533            assert_eq!(options.max_total_bytes(), Some(limit));
534            assert_eq!(options.max_total_bytes_or(7), bytes);
535        }
536        options = ReadOptions::new();
537        assert_eq!(options.max_total_bytes(), None);
538        assert_eq!(options.max_total_bytes_or(7), 7);
539        assert_eq!(
540            options.max_total_bytes_or(0),
541            0,
542            "an inherited zero policy must not become unlimited"
543        );
544    }
545}
546
547#[cfg(test)]
548mod compact_read_tests {
549    use super::*;
550    #[test]
551    fn compact_read_results_preserve_owned_and_buffered_empty_progress() {
552        if cfg!(target_pointer_width = "64") {
553            assert_eq!(std::mem::size_of::<ReadResult>(), 40);
554        }
555        let owned = ReadResult::owned(7, Vec::new(), true);
556        const BUFFERED: ReadResult = ReadResult::buffered(7, 0, true);
557        assert_eq!(owned.bytes_read(), 0);
558        assert_eq!(owned.offset(), BUFFERED.offset());
559        assert_eq!(owned.eof(), BUFFERED.eof());
560        assert_eq!(owned.data(), Some(b"".as_slice()));
561        assert_eq!(BUFFERED.data(), None);
562        assert_ne!(owned, BUFFERED);
563        assert!(!owned.is_buffered());
564        assert!(BUFFERED.is_buffered());
565        assert_eq!(owned.clone().into_data(), Some(Vec::new()));
566        assert_eq!(BUFFERED.clone().into_data(), None);
567        let data = vec![1, 2, 3];
568        let pointer = data.as_ptr();
569        let result = ReadResult::owned(9, data, false);
570        assert_eq!(result.bytes_read(), 3);
571        let extracted = result.into_data().unwrap();
572        assert_eq!(extracted.as_ptr(), pointer);
573        assert_eq!(
574            ReadResult::buffered(0, usize::MAX, false).bytes_read(),
575            usize::MAX
576        );
577    }
578}