Skip to main content

incular_platform/
content_sensitivity.rs

1//! Platform-neutral content-capture protection boundaries.
2//!
3//! Window privacy policy is deliberately expressed as normalized data here.
4//! Native adapters decide whether the host can enforce it, while headless
5//! embedders can use the in-memory backend to exercise the same state machine.
6
7use incular_config::ContentSensitivity;
8
9/// Whether a native backend can enforce content sensitivity for a window.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
11pub enum ContentSensitivityCapability {
12    /// The backend can apply all normalized sensitivity values.
13    Supported,
14    /// The backend has no capture-protection primitive for this window.
15    #[default]
16    Unsupported,
17}
18
19/// Why a content-sensitivity request was intentionally not applied.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub enum ContentSensitivityNoOpReason {
22    /// The native backend does not expose capture-protection support.
23    Unsupported,
24    /// The requested value was already active.
25    Unchanged,
26}
27
28/// Result of applying a normalized content-sensitivity request.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum ContentSensitivityOutcome {
31    /// The backend accepted and made the requested value active.
32    Applied { sensitivity: ContentSensitivity },
33    /// The request was observed but did not change native state.
34    NoOp {
35        requested: ContentSensitivity,
36        reason: ContentSensitivityNoOpReason,
37    },
38}
39
40impl ContentSensitivityOutcome {
41    /// Returns the request's normalized value regardless of whether native
42    /// state changed.
43    #[must_use]
44    pub const fn requested(self) -> ContentSensitivity {
45        match self {
46            Self::Applied { sensitivity } => sensitivity,
47            Self::NoOp { requested, .. } => requested,
48        }
49    }
50
51    /// Returns whether the native backend did not change state.
52    #[must_use]
53    pub const fn is_noop(self) -> bool {
54        matches!(self, Self::NoOp { .. })
55    }
56}
57
58/// Native capability seam for applying window content sensitivity.
59pub trait ContentSensitivityBackend {
60    /// Reports support without touching native state.
61    fn capability(&self) -> ContentSensitivityCapability;
62
63    /// Applies one normalized request and reports an explicit no-op when the
64    /// backend cannot enforce it or the value is already active.
65    fn apply(&mut self, sensitivity: ContentSensitivity) -> ContentSensitivityOutcome;
66}
67
68/// Explicit unsupported backend used by platforms without a capture policy
69/// primitive. Keeping this as a real backend makes unsupported behavior
70/// observable and testable rather than silently dropping a command.
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
72pub struct NoopContentSensitivityBackend;
73
74impl ContentSensitivityBackend for NoopContentSensitivityBackend {
75    fn capability(&self) -> ContentSensitivityCapability {
76        ContentSensitivityCapability::Unsupported
77    }
78
79    fn apply(&mut self, sensitivity: ContentSensitivity) -> ContentSensitivityOutcome {
80        ContentSensitivityOutcome::NoOp {
81            requested: sensitivity,
82            reason: ContentSensitivityNoOpReason::Unsupported,
83        }
84    }
85}
86
87/// Deterministic backend for headless hosts and platform-boundary tests.
88///
89/// A supported instance records every request and reports `Unchanged` for a
90/// duplicate request, matching the coalescing expected at a native boundary.
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct MemoryContentSensitivityBackend {
93    capability: ContentSensitivityCapability,
94    current: ContentSensitivity,
95    outcomes: Vec<ContentSensitivityOutcome>,
96}
97
98impl MemoryContentSensitivityBackend {
99    /// Creates a backend with the requested capability and a neutral initial
100    /// window policy.
101    #[must_use]
102    pub fn new(capability: ContentSensitivityCapability) -> Self {
103        Self {
104            capability,
105            current: ContentSensitivity::NotSensitive,
106            outcomes: Vec::new(),
107        }
108    }
109
110    /// Creates a backend that accepts capture-protection requests.
111    #[must_use]
112    pub fn supported() -> Self {
113        Self::new(ContentSensitivityCapability::Supported)
114    }
115
116    /// Creates a backend that reports every request as unsupported.
117    #[must_use]
118    pub fn unsupported() -> Self {
119        Self::new(ContentSensitivityCapability::Unsupported)
120    }
121
122    /// Returns the most recently applied supported value.
123    #[must_use]
124    pub const fn current(&self) -> ContentSensitivity {
125        self.current
126    }
127
128    /// Returns all outcomes in request order.
129    #[must_use]
130    pub fn outcomes(&self) -> &[ContentSensitivityOutcome] {
131        &self.outcomes
132    }
133
134    /// Clears recorded outcomes without changing the simulated native value.
135    pub fn clear_outcomes(&mut self) {
136        self.outcomes.clear();
137    }
138}
139
140impl Default for MemoryContentSensitivityBackend {
141    fn default() -> Self {
142        Self::supported()
143    }
144}
145
146impl ContentSensitivityBackend for MemoryContentSensitivityBackend {
147    fn capability(&self) -> ContentSensitivityCapability {
148        self.capability
149    }
150
151    fn apply(&mut self, sensitivity: ContentSensitivity) -> ContentSensitivityOutcome {
152        let outcome = match self.capability {
153            ContentSensitivityCapability::Unsupported => ContentSensitivityOutcome::NoOp {
154                requested: sensitivity,
155                reason: ContentSensitivityNoOpReason::Unsupported,
156            },
157            ContentSensitivityCapability::Supported if self.current == sensitivity => {
158                ContentSensitivityOutcome::NoOp {
159                    requested: sensitivity,
160                    reason: ContentSensitivityNoOpReason::Unchanged,
161                }
162            }
163            ContentSensitivityCapability::Supported => {
164                self.current = sensitivity;
165                ContentSensitivityOutcome::Applied { sensitivity }
166            }
167        };
168        self.outcomes.push(outcome);
169        outcome
170    }
171}