Skip to main content

fs_core/
readonly.rs

1//! Read-only safety wrapper.
2//!
3//! [`ReadOnlyDevice`] wraps any `BlockRead` and presents it as a
4//! [`BlockDevice`] whose write path is unconditionally
5//! [`Error::ReadOnly`] and whose `is_writable()` is always `false` —
6//! regardless of what the underlying device supports.
7//!
8//! Useful when the caller wants type-level certainty that no writes can
9//! land on a particular device, even if the underlying type *could*
10//! accept them. Examples: "snapshot view" of a writable image, an
11//! inspection tool that must never mutate, a slice handed across an FFI
12//! boundary that the consumer should not be able to write through.
13
14use crate::block::{BlockDevice, BlockRead};
15use crate::error::Result;
16
17/// Wraps any `T: BlockRead` and makes it read-only at the type level.
18pub struct ReadOnlyDevice<T> {
19    inner: T,
20}
21
22impl<T> ReadOnlyDevice<T> {
23    pub fn new(inner: T) -> Self {
24        Self { inner }
25    }
26
27    /// Borrow the wrapped device.
28    pub fn inner(&self) -> &T {
29        &self.inner
30    }
31
32    /// Consume the wrapper and return the inner device unchanged.
33    pub fn into_inner(self) -> T {
34        self.inner
35    }
36}
37
38impl<T: BlockRead> BlockRead for ReadOnlyDevice<T> {
39    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
40        self.inner.read_at(offset, buf)
41    }
42
43    fn size_bytes(&self) -> u64 {
44        self.inner.size_bytes()
45    }
46}
47
48/// `BlockDevice` impl uses the trait's default (`Err(ReadOnly)` for
49/// `write_at`, no-op `flush`, `is_writable() -> false`). Even if `T`
50/// implements `BlockDevice` with full writes, the wrapper hides that.
51impl<T: BlockRead> BlockDevice for ReadOnlyDevice<T> {}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::error::Error;
57    use std::sync::Mutex;
58
59    /// Pretend-writable inner for testing the read-only wrapper.
60    struct WritableBytes(Mutex<Vec<u8>>);
61    impl BlockRead for WritableBytes {
62        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
63            let b = self.0.lock().unwrap();
64            let s = offset as usize;
65            buf.copy_from_slice(&b[s..s + buf.len()]);
66            Ok(())
67        }
68        fn size_bytes(&self) -> u64 {
69            self.0.lock().unwrap().len() as u64
70        }
71    }
72    impl BlockDevice for WritableBytes {
73        fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
74            let mut b = self.0.lock().unwrap();
75            let s = offset as usize;
76            b[s..s + buf.len()].copy_from_slice(buf);
77            Ok(())
78        }
79        fn is_writable(&self) -> bool {
80            true
81        }
82    }
83
84    #[test]
85    fn read_through_works() {
86        let mut v = vec![0u8; 16];
87        v[4..8].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
88        let wrapped = ReadOnlyDevice::new(WritableBytes(Mutex::new(v)));
89
90        let mut buf = [0u8; 4];
91        wrapped.read_at(4, &mut buf).unwrap();
92        assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
93    }
94
95    #[test]
96    fn writes_rejected_even_though_inner_is_writable() {
97        let wrapped = ReadOnlyDevice::new(WritableBytes(Mutex::new(vec![0u8; 16])));
98        assert!(!BlockDevice::is_writable(&wrapped));
99
100        match BlockDevice::write_at(&wrapped, 0, &[0xFFu8; 4]) {
101            Err(Error::ReadOnly) => {}
102            other => panic!("expected ReadOnly, got {other:?}"),
103        }
104    }
105
106    #[test]
107    fn into_inner_returns_unchanged() {
108        let inner = WritableBytes(Mutex::new(vec![0u8; 8]));
109        let wrapped = ReadOnlyDevice::new(inner);
110        let back = wrapped.into_inner();
111        // Inner should still be writable when accessed directly.
112        assert!(BlockDevice::is_writable(&back));
113    }
114}