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 crate::test_device::RwBytes as WritableBytes;
58    use std::sync::Mutex;
59
60    #[test]
61    fn read_through_works() {
62        let mut v = vec![0u8; 16];
63        v[4..8].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
64        let wrapped = ReadOnlyDevice::new(WritableBytes(Mutex::new(v)));
65
66        let mut buf = [0u8; 4];
67        wrapped.read_at(4, &mut buf).unwrap();
68        assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
69    }
70
71    #[test]
72    fn writes_rejected_even_though_inner_is_writable() {
73        let wrapped = ReadOnlyDevice::new(WritableBytes(Mutex::new(vec![0u8; 16])));
74        assert!(!BlockDevice::is_writable(&wrapped));
75
76        match BlockDevice::write_at(&wrapped, 0, &[0xFFu8; 4]) {
77            Err(Error::ReadOnly) => {}
78            other => panic!("expected ReadOnly, got {other:?}"),
79        }
80    }
81
82    #[test]
83    fn into_inner_returns_unchanged() {
84        let inner = WritableBytes(Mutex::new(vec![0u8; 8]));
85        let wrapped = ReadOnlyDevice::new(inner);
86        let back = wrapped.into_inner();
87        // Inner should still be writable when accessed directly.
88        assert!(BlockDevice::is_writable(&back));
89        // The recovered inner still accepts a direct write + read-back.
90        back.write_at(0, &[0x42; 4]).unwrap();
91        let mut buf = [0u8; 4];
92        back.read_at(0, &mut buf).unwrap();
93        assert_eq!(buf, [0x42; 4]);
94    }
95
96    #[test]
97    fn inner_borrows_without_consuming() {
98        let mut v = vec![0u8; 8];
99        v[0..2].copy_from_slice(&[0x9A, 0xBC]);
100        let wrapped = ReadOnlyDevice::new(WritableBytes(Mutex::new(v)));
101
102        // `inner()` exposes the underlying device by reference; the inner
103        // type's own (writable) behaviour is visible through it.
104        assert!(BlockDevice::is_writable(wrapped.inner()));
105        assert_eq!(wrapped.inner().size_bytes(), 8);
106
107        // Wrapper still usable after borrowing the inner.
108        let mut buf = [0u8; 2];
109        wrapped.read_at(0, &mut buf).unwrap();
110        assert_eq!(buf, [0x9A, 0xBC]);
111    }
112}