Skip to main content

lab_proxy_core/
provider.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Provider-neutral bundle of acquired resources.
6//!
7//! A resource provider (for example the platform-device adapter in the
8//! driver crate) discovers the resources offered to the bound node and
9//! produces one [`ProvidedResources`]: descriptions, the immutable
10//! ceiling entries that apply to them, and a backend per resource. The
11//! bundle is validated before the proxy serves anything, so a provider
12//! bug fails driver start instead of producing a resource without policy
13//! or a policy without a resource.
14
15use crate::access_policy::{
16    MmioResource, ProtocolCeiling, ResourceCeiling, ResourceId, ResourceKind,
17};
18use crate::digest::{Sha256Digest, combined_digest, per_resource_digests};
19use std::collections::BTreeMap;
20
21/// Provider kind used by the embedded driver library (Phase 2).
22pub const EMBEDDED_PROVIDER: &str = "embedded";
23
24/// Everything one provider acquired for the bound node.
25#[derive(Debug)]
26pub struct ProvidedResources<B> {
27    /// Provider kind, for example `"platform"`, `"embedded"`, or `"fake"`. Feeds the
28    /// per-resource digests.
29    pub provider: String,
30    /// Stable node identity criteria (for example the node moniker),
31    /// never a per-boot numeric ID. Feeds the per-resource digests.
32    pub node_identity: String,
33    /// Logical resource descriptions, keyed by resource ordinal.
34    pub resources: BTreeMap<ResourceId, MmioResource>,
35    /// Immutable ceiling entries. A resource with no entry permits
36    /// nothing.
37    pub ceiling: BTreeMap<ResourceId, ResourceCeiling>,
38    /// One backend per offered resource.
39    pub backends: BTreeMap<ResourceId, B>,
40}
41
42/// Why a provider bundle was rejected.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum ProviderError {
45    /// A resource has no backend, or a backend has no resource.
46    ResourceBackendMismatch {
47        /// Resource ordinals present on exactly one side.
48        missing: Vec<ResourceId>,
49    },
50    /// A ceiling entry names a resource that is not offered. The reverse
51    /// (an offered resource without a ceiling entry) is legal and simply
52    /// permits nothing.
53    CeilingWithoutResource {
54        /// The dangling ceiling ordinals.
55        missing: Vec<ResourceId>,
56    },
57    /// A resource's mapped size is smaller than its logical size, so
58    /// reads the description advertises could fault.
59    MappingSmallerThanLogical {
60        /// The offending resource ordinal.
61        resource: ResourceId,
62    },
63}
64
65impl<B> ProvidedResources<B> {
66    /// An empty bundle: no resources, nothing reachable.
67    pub fn empty(provider: &str, node_identity: &str) -> Self {
68        Self {
69            provider: provider.to_string(),
70            node_identity: node_identity.to_string(),
71            resources: BTreeMap::new(),
72            ceiling: BTreeMap::new(),
73            backends: BTreeMap::new(),
74        }
75    }
76
77    /// Creates an empty bundle for an embedded in-situ driver library.
78    pub fn embedded(node_identity: &str) -> Self {
79        Self::empty(EMBEDDED_PROVIDER, node_identity)
80    }
81
82    /// Registers a pre-mapped MMIO bank with its logical size, actual mapped size,
83    /// target ceiling policy, and backend.
84    pub fn add_mmio(
85        &mut self,
86        id: ResourceId,
87        name: impl Into<String>,
88        logical_size: u64,
89        mapped_size: u64,
90        ceiling: ResourceCeiling,
91        backend: B,
92    ) {
93        self.resources.insert(id, MmioResource::mmio(name, logical_size, mapped_size));
94        self.ceiling.insert(id, ceiling);
95        self.backends.insert(id, backend);
96    }
97
98    /// Registers a protocol-backed resource (GPIO, I2C, SPI, Clock, Reset, Serial).
99    pub fn add_protocol(
100        &mut self,
101        id: ResourceId,
102        name: impl Into<String>,
103        kind: ResourceKind,
104        ceiling: ProtocolCeiling,
105        backend: B,
106    ) {
107        let resource = match kind {
108            ResourceKind::Gpio => MmioResource::gpio(name),
109            ResourceKind::I2c => MmioResource::i2c(name),
110            ResourceKind::Spi => MmioResource::spi(name),
111            ResourceKind::Clock => MmioResource::clock(name),
112            ResourceKind::Reset => MmioResource::reset(name),
113            ResourceKind::Serial => MmioResource::serial(name),
114            ResourceKind::Interrupt => MmioResource::interrupt(name),
115            ResourceKind::Mmio => MmioResource::mmio(name, 0, 0),
116        };
117        self.resources.insert(id, resource);
118        self.ceiling.insert(
119            id,
120            ResourceCeiling {
121                hard_denied: vec![],
122                allow_unknown_reads: false,
123                allow_poll: false,
124                writable_registers: vec![],
125                protocol: Some(ceiling),
126                allow_interrupt: kind == ResourceKind::Interrupt,
127            },
128        );
129        self.backends.insert(id, backend);
130    }
131
132    /// Registers an interrupt observation resource.
133    pub fn add_interrupt(&mut self, id: ResourceId, name: impl Into<String>, backend: B) {
134        self.resources.insert(id, MmioResource::interrupt(name));
135        self.ceiling.insert(
136            id,
137            ResourceCeiling {
138                hard_denied: vec![],
139                allow_unknown_reads: false,
140                allow_poll: false,
141                writable_registers: vec![],
142                protocol: Some(ProtocolCeiling::default_for(ResourceKind::Interrupt)),
143                allow_interrupt: true,
144            },
145        );
146        self.backends.insert(id, backend);
147    }
148
149    /// Validates internal consistency. Driver start fails on error.
150    pub fn validate(&self) -> Result<(), ProviderError> {
151        let resource_ids: Vec<ResourceId> = self.resources.keys().copied().collect();
152        let backend_ids: Vec<ResourceId> = self.backends.keys().copied().collect();
153        if resource_ids != backend_ids {
154            let missing = resource_ids
155                .iter()
156                .filter(|id| !self.backends.contains_key(id))
157                .chain(backend_ids.iter().filter(|id| !self.resources.contains_key(id)))
158                .copied()
159                .collect();
160            return Err(ProviderError::ResourceBackendMismatch { missing });
161        }
162        let dangling: Vec<ResourceId> =
163            self.ceiling.keys().filter(|id| !self.resources.contains_key(id)).copied().collect();
164        if !dangling.is_empty() {
165            return Err(ProviderError::CeilingWithoutResource { missing: dangling });
166        }
167        for (id, resource) in &self.resources {
168            if resource.mapped_size < resource.logical_size {
169                return Err(ProviderError::MappingSmallerThanLogical { resource: *id });
170            }
171        }
172        Ok(())
173    }
174
175    /// Per-resource digests for this bundle, in ascending resource ID
176    /// order.
177    pub fn digests(&self) -> BTreeMap<ResourceId, Sha256Digest> {
178        per_resource_digests(&self.node_identity, &self.provider, &self.resources, &self.ceiling)
179    }
180
181    /// The whole-instance resource digest for this bundle.
182    pub fn combined_digest(&self) -> Sha256Digest {
183        combined_digest(self.digests().values().copied())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::hardware_backend::FakeMmio;
191
192    fn resource(logical: u64, mapped: u64) -> MmioResource {
193        MmioResource {
194            name: "ctrl".to_string(),
195            kind: crate::access_policy::ResourceKind::Mmio,
196            logical_size: logical,
197            mapped_size: mapped,
198        }
199    }
200
201    fn bundle() -> ProvidedResources<FakeMmio> {
202        let mut bundle = ProvidedResources::empty("fake", "node");
203        bundle.resources.insert(1, resource(0x100, 0x1000));
204        bundle.ceiling.insert(
205            1,
206            ResourceCeiling {
207                hard_denied: vec![],
208                allow_unknown_reads: true,
209                allow_poll: false,
210                writable_registers: vec![],
211                protocol: None,
212                allow_interrupt: false,
213            },
214        );
215        bundle.backends.insert(1, FakeMmio::new());
216        bundle
217    }
218
219    #[test]
220    fn valid_bundle_passes() {
221        assert_eq!(bundle().validate(), Ok(()));
222        assert_eq!(ProvidedResources::<FakeMmio>::empty("fake", "node").validate(), Ok(()));
223    }
224
225    #[test]
226    fn missing_backend_is_rejected() {
227        let mut bundle = bundle();
228        bundle.backends.clear();
229        assert_eq!(
230            bundle.validate(),
231            Err(ProviderError::ResourceBackendMismatch { missing: vec![1] })
232        );
233    }
234
235    #[test]
236    fn dangling_ceiling_is_rejected() {
237        let mut bundle = bundle();
238        bundle.ceiling.insert(
239            9,
240            ResourceCeiling {
241                hard_denied: vec![],
242                allow_unknown_reads: false,
243                allow_poll: false,
244                writable_registers: vec![],
245                protocol: None,
246                allow_interrupt: false,
247            },
248        );
249        assert_eq!(
250            bundle.validate(),
251            Err(ProviderError::CeilingWithoutResource { missing: vec![9] })
252        );
253    }
254
255    #[test]
256    fn resource_without_ceiling_is_legal() {
257        let mut bundle = bundle();
258        bundle.ceiling.clear();
259        assert_eq!(bundle.validate(), Ok(()));
260    }
261
262    #[test]
263    fn short_mapping_is_rejected() {
264        let mut bundle = bundle();
265        bundle.resources.insert(1, resource(0x1000, 0x100));
266        assert_eq!(
267            bundle.validate(),
268            Err(ProviderError::MappingSmallerThanLogical { resource: 1 })
269        );
270    }
271
272    #[test]
273    fn digests_are_ordered_and_combined() {
274        let bundle = bundle();
275        let digests = bundle.digests();
276        assert_eq!(digests.len(), 1);
277        assert_eq!(bundle.combined_digest(), combined_digest(digests.values().copied()));
278    }
279
280    #[test]
281    fn embedded_provider_registers_mmio_protocol_and_interrupt() {
282        let mut bundle = ProvidedResources::embedded("sample-driver");
283        assert_eq!(bundle.provider, EMBEDDED_PROVIDER);
284        assert_eq!(bundle.node_identity, "sample-driver");
285
286        bundle.add_mmio(
287            0,
288            "regs",
289            0x100,
290            0x1000,
291            ResourceCeiling {
292                hard_denied: vec![0x40..0x44],
293                allow_unknown_reads: true,
294                allow_poll: true,
295                writable_registers: vec![],
296                protocol: None,
297                allow_interrupt: false,
298            },
299            FakeMmio::new(),
300        );
301        bundle.add_protocol(
302            1,
303            "gpio0",
304            ResourceKind::Gpio,
305            ProtocolCeiling::default_for(ResourceKind::Gpio),
306            FakeMmio::new(),
307        );
308        bundle.add_interrupt(2, "irq0", FakeMmio::new());
309
310        assert_eq!(bundle.validate(), Ok(()));
311        assert_eq!(bundle.resources.len(), 3);
312        assert_eq!(bundle.resources[&0].kind, ResourceKind::Mmio);
313        assert_eq!(bundle.resources[&1].kind, ResourceKind::Gpio);
314        assert_eq!(bundle.resources[&2].kind, ResourceKind::Interrupt);
315        assert!(bundle.ceiling[&2].allow_interrupt);
316    }
317}