embedded_shadow/view/
host_staged.rs

1use crate::{AccessPolicy, HostView, PersistTrigger, policy::PersistPolicy, types::StagingBuffer};
2use bitmaps::{Bits, BitsImpl};
3
4/// Host view with transactional staging support.
5///
6/// Allows writes to be staged and previewed before committing to the shadow table.
7pub struct HostViewStaged<'a, const TS: usize, const BS: usize, const BC: usize, AP, PP, PT, PK, SB>
8where
9    AP: AccessPolicy,
10    PP: PersistPolicy<PK>,
11    PT: PersistTrigger<PK>,
12    BitsImpl<BC>: Bits,
13    SB: StagingBuffer,
14{
15    base: HostView<'a, TS, BS, BC, AP, PP, PT, PK>,
16    sb: &'a mut SB,
17}
18
19impl<'a, const TS: usize, const BS: usize, const BC: usize, AP, PP, PT, PK, SB>
20    HostViewStaged<'a, TS, BS, BC, AP, PP, PT, PK, SB>
21where
22    AP: AccessPolicy,
23    PP: PersistPolicy<PK>,
24    PT: PersistTrigger<PK>,
25    BitsImpl<BC>: Bits,
26    SB: StagingBuffer,
27{
28    pub(crate) fn new(base: HostView<'a, TS, BS, BC, AP, PP, PT, PK>, sb: &'a mut SB) -> Self {
29        Self { base, sb }
30    }
31
32    /// Reads data from the shadow table (ignores staged writes).
33    pub fn read_range(&self, addr: u16, out: &mut [u8]) -> Result<(), crate::ShadowError> {
34        self.base.read_range(addr, out)
35    }
36
37    /// Writes directly to the shadow table, bypassing staging.
38    pub fn write_range(&mut self, addr: u16, data: &[u8]) -> Result<(), crate::ShadowError> {
39        self.base.write_range(addr, data)
40    }
41
42    /// Reads data with staged writes overlaid on top.
43    pub fn read_range_overlay(&self, addr: u16, out: &mut [u8]) -> Result<(), crate::ShadowError> {
44        if !self.base.access_policy.can_read(addr, out.len()) {
45            return Err(crate::ShadowError::Denied);
46        }
47
48        self.base.read_range(addr, out)?;
49        self.sb.apply_overlay(addr, out)?;
50        Ok(())
51    }
52
53    /// Stages a write to be applied on commit.
54    pub fn write_range_staged(&mut self, addr: u16, data: &[u8]) -> Result<(), crate::ShadowError> {
55        if !self.base.access_policy.can_write(addr, data.len()) {
56            return Err(crate::ShadowError::Denied);
57        }
58
59        self.sb.write_staged(addr, data)
60    }
61
62    /// Commits all staged writes to the shadow table.
63    ///
64    /// Staged writes are applied in order, marking blocks dirty and
65    /// triggering persistence as configured. The staging buffer is
66    /// cleared after successful commit.
67    pub fn commit(&mut self) -> Result<(), crate::ShadowError> {
68        if !self.sb.any_staged() {
69            return Ok(());
70        }
71
72        let mut should_persist = false;
73        self.sb.for_each_staged(|addr, data| {
74            self.base.write_range_no_persist(addr, data)?;
75            should_persist |=
76                self.base
77                    .persist_policy
78                    .push_persist_keys_for_range(addr, data.len(), |key| {
79                        self.base.persist_trigger.push_key(key)
80                    });
81            Ok(())
82        })?;
83
84        self.sb.clear_staged()?;
85
86        if should_persist {
87            self.base.persist_trigger.request_persist();
88        }
89
90        Ok(())
91    }
92
93    /// Commits all staged writes to the shadow table.
94    #[deprecated(since = "0.1.2", note = "renamed to `commit()`")]
95    pub fn action(&mut self) -> Result<(), crate::ShadowError> {
96        self.commit()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use crate::ShadowError;
103    use crate::persist::NoPersist;
104    use crate::policy::{AllowAllPolicy, NoPersistPolicy};
105    use crate::staged::PatchStagingBuffer;
106    use crate::test_support::{DenyAllPolicy, TestTable};
107    use crate::view::HostView;
108
109    use super::*;
110
111    type TestStage = PatchStagingBuffer<64, 8>;
112
113    #[test]
114    fn commit_applies_staged_writes_to_table() {
115        let mut table = TestTable::new();
116        let policy = AllowAllPolicy::default();
117        let persist_policy = NoPersistPolicy::default();
118        let mut trigger = NoPersist;
119        let mut stage = TestStage::new();
120
121        {
122            let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
123            let mut view = HostViewStaged::new(base, &mut stage);
124
125            view.write_range_staged(0, &[0xAA, 0xBB, 0xCC, 0xDD])
126                .unwrap();
127            view.commit().unwrap();
128        }
129
130        // Data should be in the table
131        let mut buf = [0u8; 4];
132        table.read_range(0, &mut buf).unwrap();
133        assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
134    }
135
136    #[test]
137    fn commit_marks_affected_blocks_dirty() {
138        let mut table = TestTable::new();
139        let policy = AllowAllPolicy::default();
140        let persist_policy = NoPersistPolicy::default();
141        let mut trigger = NoPersist;
142        let mut stage = TestStage::new();
143
144        // Stage data but don't commit yet
145        {
146            let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
147            let mut view = HostViewStaged::new(base, &mut stage);
148            view.write_range_staged(0, &[0x01; 4]).unwrap();
149        }
150
151        // Not dirty yet (staged only)
152        assert!(!table.any_dirty());
153
154        // Now commit
155        {
156            let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
157            let mut view = HostViewStaged::new(base, &mut stage);
158            view.commit().unwrap();
159        }
160
161        assert!(table.is_dirty(0, 4).unwrap());
162    }
163
164    #[test]
165    fn commit_clears_staging_buffer() {
166        let mut table = TestTable::new();
167        let policy = AllowAllPolicy::default();
168        let persist_policy = NoPersistPolicy::default();
169        let mut trigger = NoPersist;
170        let mut stage = TestStage::new();
171
172        {
173            let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
174            let mut view = HostViewStaged::new(base, &mut stage);
175
176            view.write_range_staged(0, &[0x01; 4]).unwrap();
177            view.commit().unwrap();
178        }
179
180        assert!(!stage.any_staged());
181    }
182
183    #[test]
184    fn commit_empty_buffer_is_noop() {
185        let mut table = TestTable::new();
186        let policy = AllowAllPolicy::default();
187        let persist_policy = NoPersistPolicy::default();
188        let mut trigger = NoPersist;
189        let mut stage = TestStage::new();
190
191        {
192            let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
193            let mut view = HostViewStaged::new(base, &mut stage);
194
195            // Commit without staging anything
196            view.commit().unwrap();
197        }
198
199        assert!(!table.any_dirty());
200    }
201
202    #[test]
203    fn read_range_overlay_shows_staged_data() {
204        let mut table = TestTable::new();
205        table.write_range(0, &[0x11, 0x22, 0x33, 0x44]).unwrap();
206
207        let policy = AllowAllPolicy::default();
208        let persist_policy = NoPersistPolicy::default();
209        let mut trigger = NoPersist;
210        let mut stage = TestStage::new();
211
212        let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
213        let mut view = HostViewStaged::new(base, &mut stage);
214
215        // Stage a write that partially overlaps
216        view.write_range_staged(2, &[0xAA, 0xBB]).unwrap();
217
218        let mut buf = [0u8; 4];
219        view.read_range_overlay(0, &mut buf).unwrap();
220
221        // First two from table, last two from staged
222        assert_eq!(buf, [0x11, 0x22, 0xAA, 0xBB]);
223    }
224
225    #[test]
226    fn read_range_ignores_staged_data() {
227        let mut table = TestTable::new();
228        table.write_range(0, &[0x11, 0x22, 0x33, 0x44]).unwrap();
229
230        let policy = AllowAllPolicy::default();
231        let persist_policy = NoPersistPolicy::default();
232        let mut trigger = NoPersist;
233        let mut stage = TestStage::new();
234
235        let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
236        let mut view = HostViewStaged::new(base, &mut stage);
237
238        // Stage a write
239        view.write_range_staged(0, &[0xAA; 4]).unwrap();
240
241        // Regular read should show table data, not staged
242        let mut buf = [0u8; 4];
243        view.read_range(0, &mut buf).unwrap();
244        assert_eq!(buf, [0x11, 0x22, 0x33, 0x44]);
245    }
246
247    #[test]
248    fn staged_write_checks_access_policy() {
249        let mut table = TestTable::new();
250        let policy = DenyAllPolicy;
251        let persist_policy = NoPersistPolicy::default();
252        let mut trigger = NoPersist;
253        let mut stage = TestStage::new();
254
255        let base = HostView::new(&mut table, &policy, &persist_policy, &mut trigger);
256        let mut view = HostViewStaged::new(base, &mut stage);
257
258        assert_eq!(
259            view.write_range_staged(0, &[0x01; 4]),
260            Err(ShadowError::Denied)
261        );
262
263        // Nothing should be staged
264        assert!(!stage.any_staged());
265    }
266}