Skip to main content

molframe_core/io/input/
source.rs

1//! Stable bounded windows over resident and file-backed sources.
2
3use super::InputBuffer;
4use crate::{Code, Diagnostic, ExecutionContext, MemoryReservation};
5use std::fs::File;
6use std::io::{ErrorKind, Read, Seek, SeekFrom};
7use std::path::Path;
8
9/// One stable view into a source.
10#[derive(Clone, Copy, Debug)]
11pub struct ByteWindow<'a> {
12    start: u64,
13    bytes: &'a [u8],
14}
15
16impl<'a> ByteWindow<'a> {
17    /// Creates a view whose first byte has the supplied global offset.
18    #[must_use]
19    pub const fn new(start: u64, bytes: &'a [u8]) -> Self {
20        Self { start, bytes }
21    }
22
23    /// Global offset of the first byte.
24    #[must_use]
25    pub const fn start(self) -> u64 {
26        self.start
27    }
28
29    /// Bytes present in this window. A final window may be shorter than demand.
30    #[must_use]
31    pub const fn bytes(self) -> &'a [u8] {
32        self.bytes
33    }
34
35    /// Exclusive global end offset.
36    ///
37    /// # Errors
38    ///
39    /// Returns `MOLFRAME-E1903` if the exclusive end would exceed `u64::MAX`.
40    pub fn end(self) -> Result<u64, Diagnostic> {
41        let length =
42            u64::try_from(self.bytes.len()).map_err(|_| unrepresentable(self.bytes.len()))?;
43        self.start
44            .checked_add(length)
45            .ok_or_else(|| unrepresentable("an offset greater than u64::MAX"))
46    }
47}
48
49/// Random-access bytes exposed through bounded, borrow-stable windows.
50pub trait SourceBytes {
51    /// Returns at most `len` bytes starting at the global byte offset `start`.
52    ///
53    /// # Errors
54    ///
55    /// Returns a registered resource, budget, or addressability diagnostic.
56    fn window(&mut self, start: u64, len: usize) -> Result<ByteWindow<'_>, Diagnostic>;
57
58    /// Exact source length when known without consuming the source.
59    fn len_hint(&self) -> Option<u64>;
60}
61
62impl SourceBytes for InputBuffer {
63    fn window(&mut self, start: u64, len: usize) -> Result<ByteWindow<'_>, Diagnostic> {
64        let local = usize::try_from(start).map_err(|_| unrepresentable(start))?;
65        let bytes = self.as_bytes();
66        if local >= bytes.len() {
67            return Ok(ByteWindow::new(start, &[]));
68        }
69        let end = local.saturating_add(len).min(bytes.len());
70        Ok(ByteWindow::new(start, &bytes[local..end]))
71    }
72
73    fn len_hint(&self) -> Option<u64> {
74        u64::try_from(self.len()).ok()
75    }
76}
77
78/// A seekable file read through one reusable, budget-charged window.
79#[derive(Debug)]
80pub struct WindowedFile {
81    file: File,
82    length: u64,
83    buffer: Box<[u8]>,
84    _reservation: MemoryReservation,
85}
86
87impl WindowedFile {
88    /// Opens a file and reserves the complete reusable window up front.
89    ///
90    /// # Errors
91    ///
92    /// Returns `MOLFRAME-E1902` if the window does not fit the shared execution
93    /// budget, or `MOLFRAME-E7101` if the file or buffer cannot be opened.
94    pub fn open(
95        path: impl AsRef<Path>,
96        max_window_bytes: usize,
97        context: &ExecutionContext,
98    ) -> Result<Self, Diagnostic> {
99        if max_window_bytes == 0 {
100            return Err(Diagnostic::new(Code::E1902).with_context("requested bytes", "0"));
101        }
102        let reservation = context.try_reserve(max_window_bytes).map_err(|error| {
103            Diagnostic::new(Code::E1902).with_context("reason", error.to_string())
104        })?;
105        let file = File::open(path.as_ref())
106            .map_err(|error| io_failure("source file could not be opened", &error))?;
107        let length = file
108            .metadata()
109            .map_err(|error| io_failure("source length could not be read", &error))?
110            .len();
111        let mut buffer = Vec::new();
112        buffer
113            .try_reserve_exact(max_window_bytes)
114            .map_err(|error| {
115                Diagnostic::new(Code::E1901).with_context("reason", error.to_string())
116            })?;
117        buffer.resize(max_window_bytes, 0);
118        Ok(Self {
119            file,
120            length,
121            buffer: buffer.into_boxed_slice(),
122            _reservation: reservation,
123        })
124    }
125
126    /// Maximum resident source-window capacity.
127    #[must_use]
128    pub const fn capacity(&self) -> usize {
129        self.buffer.len()
130    }
131}
132
133impl SourceBytes for WindowedFile {
134    fn window(&mut self, start: u64, len: usize) -> Result<ByteWindow<'_>, Diagnostic> {
135        if len > self.buffer.len() {
136            return Err(Diagnostic::new(Code::E1902)
137                .with_context("requested bytes", len.to_string())
138                .with_context("window bytes", self.buffer.len().to_string()));
139        }
140        if start >= self.length || len == 0 {
141            return Ok(ByteWindow::new(start, &[]));
142        }
143        self.file
144            .seek(SeekFrom::Start(start))
145            .map_err(|error| io_failure("source window could not seek", &error))?;
146        let remaining = self.length - start;
147        let host_remaining = match usize::try_from(remaining) {
148            Ok(value) => value,
149            Err(_) => usize::MAX,
150        };
151        let target = len.min(host_remaining);
152        let mut filled = 0;
153        while filled < target {
154            match self.file.read(&mut self.buffer[filled..target]) {
155                Ok(0) => break,
156                Ok(count) => filled += count,
157                Err(error) if error.kind() == ErrorKind::Interrupted => {}
158                Err(error) => return Err(io_failure("source window could not be read", &error)),
159            }
160        }
161        Ok(ByteWindow::new(start, &self.buffer[..filled]))
162    }
163
164    fn len_hint(&self) -> Option<u64> {
165        Some(self.length)
166    }
167}
168
169fn io_failure(message: &'static str, error: &std::io::Error) -> Diagnostic {
170    Diagnostic::new(Code::E7101)
171        .with_message(message)
172        .with_context("reason", error.to_string())
173}
174
175fn unrepresentable(value: impl std::fmt::Display) -> Diagnostic {
176    Diagnostic::new(Code::E1903).with_context("value", value.to_string())
177}
178
179#[cfg(test)]
180#[path = "source_tests.rs"]
181mod tests;