embedded_shadow/view/
host.rs

1use core::marker::PhantomData;
2
3use crate::{AccessPolicy, PersistTrigger, ShadowError, policy::PersistPolicy, table::ShadowTable};
4use bitmaps::{Bits, BitsImpl};
5
6/// Application/host-side view of the shadow table.
7///
8/// Writes through this view mark blocks dirty and may trigger persistence.
9/// Reads and writes are subject to the configured access policy.
10pub struct HostView<'a, const TS: usize, const BS: usize, const BC: usize, AP, PP, PT, PK>
11where
12    BitsImpl<BC>: Bits,
13    AP: AccessPolicy,
14    PP: PersistPolicy<PK>,
15    PT: PersistTrigger<PK>,
16{
17    pub(crate) table: &'a mut ShadowTable<TS, BS, BC>,
18    pub(crate) access_policy: &'a AP,
19    pub(crate) persist_policy: &'a PP,
20    pub(crate) persist_trigger: &'a mut PT,
21    _phantom: PhantomData<PK>,
22}
23
24impl<'a, const TS: usize, const BS: usize, const BC: usize, AP, PP, PT, PK>
25    HostView<'a, TS, BS, BC, AP, PP, PT, PK>
26where
27    BitsImpl<BC>: Bits,
28    AP: AccessPolicy,
29    PP: PersistPolicy<PK>,
30    PT: PersistTrigger<PK>,
31{
32    pub(crate) fn new(
33        table: &'a mut ShadowTable<TS, BS, BC>,
34        access_policy: &'a AP,
35        persist_policy: &'a PP,
36        persist_trigger: &'a mut PT,
37    ) -> Self {
38        Self {
39            table,
40            access_policy,
41            persist_policy,
42            persist_trigger,
43            _phantom: PhantomData,
44        }
45    }
46
47    /// Reads data from the shadow table.
48    ///
49    /// Returns `Denied` if the access policy rejects the read.
50    pub fn read_range(&self, addr: u16, out: &mut [u8]) -> Result<(), ShadowError> {
51        if !self.access_policy.can_read(addr, out.len()) {
52            return Err(ShadowError::Denied);
53        }
54        self.table.read_range(addr, out)
55    }
56
57    /// Writes data to the shadow table, marking blocks dirty.
58    ///
59    /// May trigger persistence based on the configured policy.
60    pub fn write_range(&mut self, addr: u16, data: &[u8]) -> Result<(), ShadowError> {
61        self.write_range_no_persist(addr, data)?;
62
63        let should_persist =
64            self.persist_policy
65                .push_persist_keys_for_range(addr, data.len(), |key| {
66                    self.persist_trigger.push_key(key)
67                });
68
69        if should_persist {
70            self.persist_trigger.request_persist();
71        }
72
73        Ok(())
74    }
75
76    pub(crate) fn write_range_no_persist(
77        &mut self,
78        addr: u16,
79        data: &[u8],
80    ) -> Result<(), ShadowError> {
81        if !self.access_policy.can_write(addr, data.len()) {
82            return Err(ShadowError::Denied);
83        }
84
85        self.table.write_range(addr, data)?;
86        self.table.mark_dirty(addr, data.len())?;
87
88        Ok(())
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::persist::NoPersist;
96    use crate::policy::{AllowAllPolicy, NoPersistPolicy};
97    use crate::test_support::{DenyAllPolicy, ReadOnlyBelow32, TestTable};
98
99    #[test]
100    fn host_write_marks_dirty() {
101        let mut table = TestTable::new();
102        let policy = AllowAllPolicy::default();
103        let persist_policy = NoPersistPolicy::default();
104        let mut trigger = NoPersist;
105
106        {
107            let mut view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
108            view.write_range(0, &[0x01, 0x02, 0x03, 0x04]).unwrap();
109        }
110
111        assert!(table.is_dirty(0, 4).unwrap());
112    }
113
114    #[test]
115    fn host_write_spanning_blocks_marks_all_dirty() {
116        let mut table = TestTable::new();
117        let policy = AllowAllPolicy::default();
118        let persist_policy = NoPersistPolicy::default();
119        let mut trigger = NoPersist;
120
121        {
122            let mut view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
123            // Write spans blocks 0 and 1 (bytes 8-23)
124            view.write_range(8, &[0xAA; 16]).unwrap();
125        }
126
127        // Both block 0 and block 1 should be dirty
128        assert!(table.is_dirty(0, 16).unwrap()); // block 0
129        assert!(table.is_dirty(16, 16).unwrap()); // block 1
130    }
131
132    #[test]
133    fn host_read_does_not_mark_dirty() {
134        let mut table = TestTable::new();
135        let policy = AllowAllPolicy::default();
136        let persist_policy = NoPersistPolicy::default();
137        let mut trigger = NoPersist;
138
139        let view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
140
141        let mut buf = [0u8; 4];
142        view.read_range(0, &mut buf).unwrap();
143
144        assert!(!table.any_dirty());
145    }
146
147    #[test]
148    fn read_denied_returns_error() {
149        let mut table = TestTable::new();
150        let policy = DenyAllPolicy;
151        let persist_policy = NoPersistPolicy::default();
152        let mut trigger = NoPersist;
153
154        let view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
155        let mut buf = [0u8; 4];
156
157        assert_eq!(view.read_range(0, &mut buf), Err(ShadowError::Denied));
158    }
159
160    #[test]
161    fn write_denied_returns_error() {
162        let mut table = TestTable::new();
163        let policy = DenyAllPolicy;
164        let persist_policy = NoPersistPolicy::default();
165        let mut trigger = NoPersist;
166
167        let mut view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
168
169        assert_eq!(view.write_range(0, &[0x01, 0x02]), Err(ShadowError::Denied));
170    }
171
172    #[test]
173    fn denied_write_does_not_modify_state() {
174        let mut table = TestTable::new();
175        table.write_range(0, &[0xFF; 4]).unwrap();
176
177        let policy = DenyAllPolicy;
178        let persist_policy = NoPersistPolicy::default();
179        let mut trigger = NoPersist;
180
181        {
182            let mut view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
183            let _ = view.write_range(0, &[0x00; 4]); // Should fail
184        }
185
186        // Original data unchanged
187        let mut buf = [0u8; 4];
188        table.read_range(0, &mut buf).unwrap();
189        assert_eq!(buf, [0xFF; 4]);
190
191        // No dirty bits set
192        assert!(!table.any_dirty());
193    }
194
195    #[test]
196    fn denied_read_does_not_leak_data() {
197        let mut table = TestTable::new();
198        table.write_range(0, &[0xAA; 4]).unwrap();
199
200        let policy = DenyAllPolicy;
201        let persist_policy = NoPersistPolicy::default();
202        let mut trigger = NoPersist;
203
204        let view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
205
206        let mut buf = [0x00u8; 4];
207        let _ = view.read_range(0, &mut buf); // Should fail
208
209        // Buffer unchanged (no data leaked)
210        assert_eq!(buf, [0x00; 4]);
211    }
212
213    #[test]
214    fn partial_policy_allows_permitted_ranges() {
215        let mut table = TestTable::new();
216        let policy = ReadOnlyBelow32;
217        let persist_policy = NoPersistPolicy::default();
218        let mut trigger = NoPersist;
219
220        let mut view = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
221
222        // Read should work anywhere
223        let mut buf = [0u8; 4];
224        assert!(view.read_range(0, &mut buf).is_ok());
225        assert!(view.read_range(32, &mut buf).is_ok());
226
227        // Write below 32 should fail
228        assert_eq!(view.write_range(0, &[0x01; 4]), Err(ShadowError::Denied));
229
230        // Write at 32 and above should work
231        assert!(view.write_range(32, &[0x01; 4]).is_ok());
232    }
233}