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}