linux_memutils/
reader.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3/*!
4Semi-efficient reading of physical memory from [`/dev/mem`].
5
6[`/dev/mem`]: https://man7.org/linux/man-pages/man4/mem.4.html
7*/
8use std::cmp::min;
9use std::io;
10use std::io::{BufRead, ErrorKind, Read, Seek, SeekFrom};
11
12/// The default initial size of the internal buffer in bytes.
13const DEFAULT_BUFFER_SIZE: usize = 64 * 1024;
14
15/// A buffering reader that transparently skips inaccessible parts of a file.
16///
17/// It functions similarly to [`io::BufReader`] in that it fills an internal
18/// buffer using larger, infrequent reads but is further capable of handling
19/// intermittent permission errors by repeatedly halving the buffer size and
20/// attempting another read. Eventually, it will fall back to skipping byte
21/// by byte towards the next readable section.
22///
23/// This will occur for many regions in `/dev/mem` with a brute-force search.
24/// Skipping long inaccessible sections is very slow, so that should only be
25/// a last resort when no memory map is available.
26#[allow(clippy::module_name_repetitions)]
27pub struct SkippingBufReader<F: Read + Seek> {
28    file: F,
29
30    /// A maximum offset in the `file` to read to; the reader will return 0
31    /// bytes (i.e. an EOF) once this is reached.
32    max_offset: Option<usize>,
33
34    buffer: Vec<u8>,
35    /// The current size of the internal `buffer`.
36    buffer_size: usize,
37    /// The initial size of the buffer to revert to whenever a read succeeds.
38    initial_buffer_size: usize,
39
40    /// The number of bytes at the start of `buffer` set during the last read.
41    ///
42    /// A read may return fewer bytes than the `buffer_size` (e.g. at the end
43    /// of a file) and thus not overwrite the entire buffer. Only those newly
44    /// initialized bytes may be used; everything after is stale data.
45    valid_bytes_in_buffer: usize,
46
47    /// The position in `buffer` to which bytes have already been "consumed".
48    ///
49    /// [`SkippingBufReader::read`] may be used with an output buffer smaller
50    /// than the number of new bytes available in `buffer`. This keeps track
51    /// of such partial reads and determines when new data needs to be read.
52    read_position_in_buffer: usize,
53}
54
55impl<F: Read + Seek> SkippingBufReader<F> {
56    pub fn new(file: F, start_offset: usize, max_offset: Option<usize>) -> Self {
57        Self::with_buffer_size(DEFAULT_BUFFER_SIZE, file, start_offset, max_offset)
58    }
59
60    /// Create a reader with an internal buffer of the given initial size.
61    ///
62    /// # Panics
63    ///
64    /// This function panics if it cannot seek to the given `start_offset`
65    /// within `file`.
66    pub fn with_buffer_size(
67        buffer_size: usize,
68        mut file: F,
69        start_offset: usize,
70        max_offset: Option<usize>,
71    ) -> Self {
72        file.seek(SeekFrom::Start(start_offset as u64))
73            .expect("failed to seek to given start offset");
74
75        Self {
76            file,
77            max_offset,
78
79            buffer: Vec::with_capacity(buffer_size),
80            buffer_size,
81            initial_buffer_size: buffer_size,
82
83            valid_bytes_in_buffer: 0,
84            read_position_in_buffer: 0,
85        }
86    }
87
88    fn refill_buffer(&mut self) -> io::Result<usize> {
89        loop {
90            // If a maximum offset to read to is specified and closer to the
91            // current position than the buffer size, reduce the buffer size
92            // to only those bytes left to read (down to 0 at the very end)
93            if let Some(max_offset) = self.max_offset {
94                let seek_position = usize::try_from(self.file.stream_position()?).unwrap();
95                let bytes_to_max_offset = max_offset - seek_position;
96                self.buffer_size = min(self.buffer_size, bytes_to_max_offset);
97            }
98
99            self.buffer.resize(self.buffer_size, 0);
100
101            match self.file.read(&mut self.buffer) {
102                Ok(0) => {
103                    log::debug!("Reached EOF or maximum offset.");
104                    return Ok(0);
105                }
106                Ok(bytes_read) => {
107                    self.buffer_size = self.initial_buffer_size;
108
109                    self.valid_bytes_in_buffer = bytes_read;
110                    self.read_position_in_buffer = 0;
111
112                    return Ok(bytes_read);
113                }
114                Err(e) if e.kind() == ErrorKind::PermissionDenied => {
115                    if self.buffer_size > 1 {
116                        self.buffer_size /= 2;
117                    } else {
118                        // Down to a 1-byte buffer; give up on this byte
119                        self.file.seek_relative(1)?;
120                    }
121                }
122                Err(e) => return Err(e),
123            }
124        }
125    }
126
127    /// Return the current read position in the file.
128    ///
129    /// # Note
130    ///
131    /// This is the position a subsequent call to `read()` will return bytes
132    /// from (or refill the buffer if empty). It is distinct from the actual
133    /// seek position in the file, which reflects the end of the latest read
134    /// performed to fill the buffer.
135    ///
136    /// # Panics
137    ///
138    /// This function panics if the current seek position in the underlying
139    /// reader cannot be obtained.
140    #[must_use]
141    pub fn position_in_file(&mut self) -> usize {
142        let seek_position = self
143            .file
144            .stream_position()
145            .expect("obtaining the current seek position should always succeed");
146        let unread_bytes_in_buffer = self.valid_bytes_in_buffer - self.read_position_in_buffer;
147
148        usize::try_from(seek_position).unwrap() - unread_bytes_in_buffer
149    }
150}
151
152impl<F: Read + Seek> BufRead for SkippingBufReader<F> {
153    fn fill_buf(&mut self) -> io::Result<&[u8]> {
154        if self.read_position_in_buffer >= self.valid_bytes_in_buffer {
155            log::trace!("No unread bytes in buffer; refilling it...");
156            let bytes_read = self.refill_buffer()?;
157            log::trace!("Refilled buffer with {} bytes.", bytes_read);
158
159            if bytes_read == 0 {
160                return Ok(&[]);
161            }
162        }
163
164        let unread_bytes = &self.buffer[self.read_position_in_buffer..self.valid_bytes_in_buffer];
165        Ok(unread_bytes)
166    }
167
168    fn consume(&mut self, amt: usize) {
169        self.read_position_in_buffer += amt;
170    }
171}
172
173impl<F: Read + Seek> Read for SkippingBufReader<F> {
174    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
175        // Fetch all unread bytes from the internal buffer, refilling
176        // it if needed
177        let mut available_bytes = self.fill_buf()?;
178        // Pull as many available bytes as fit into the output buffer
179        let no_of_read_bytes = available_bytes.read(buf)?;
180        // Advance the cursor in the internal buffer by the number of
181        // bytes that were used
182        self.consume(no_of_read_bytes);
183
184        Ok(no_of_read_bytes)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use std::io::Cursor;
192
193    #[test]
194    fn position_in_file_returns_expected_position() {
195        let file = Cursor::new("abcdefghijklmnopqrst");
196        let mut reader = SkippingBufReader::with_buffer_size(8, file, 0, None);
197
198        // This number of bytes to read falls into the last (partial) buffer
199        // fill, making it suitable for testing that the result is not based
200        // on the buffer's size but the actual number of _valid_ bytes in it
201        let mut data = [0; 18];
202        reader.read_exact(&mut data).unwrap();
203        assert_eq!(reader.position_in_file(), 18);
204    }
205
206    #[test]
207    fn stops_reading_at_max_offset_if_specified() {
208        let file = Cursor::new("abcdefghijklmnopqrst");
209        let mut reader = SkippingBufReader::with_buffer_size(8, file, 0, Some(10));
210
211        let mut data = Vec::new();
212        reader.read_to_end(&mut data).unwrap();
213        assert_eq!(data, b"abcdefghij");
214    }
215}