Skip to main content

zendriver_stealth/persona/
surface.rs

1//! Surface + Strategy + per-surface kind resolution.
2
3use serde::{Deserialize, Serialize};
4
5/// A fingerprint surface.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub enum Surface {
8    Canvas,
9    Webgl,
10    Audio,
11    Fonts,
12    ClientRects,
13    Webrtc,
14    Hardware,
15    Webgpu,
16}
17
18/// How a resolved persona is applied to a surface in the page.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum Strategy {
21    Native,
22    Seeded,
23    Random,
24    Block,
25    Value,
26}
27
28/// The semantic family a surface belongs to.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SurfaceKind {
31    Noise,
32    Value,
33    Policy,
34}
35
36impl Surface {
37    pub fn kind(self) -> SurfaceKind {
38        match self {
39            Surface::Canvas | Surface::Audio | Surface::ClientRects => SurfaceKind::Noise,
40            Surface::Webgl | Surface::Fonts | Surface::Hardware | Surface::Webgpu => {
41                SurfaceKind::Value
42            }
43            Surface::Webrtc => SurfaceKind::Policy,
44        }
45    }
46
47    /// Default strategy when none is set.
48    pub fn default_strategy(self) -> Strategy {
49        match self.kind() {
50            SurfaceKind::Noise => Strategy::Seeded,
51            SurfaceKind::Value => Strategy::Value,
52            SurfaceKind::Policy => Strategy::Block,
53        }
54    }
55
56    /// Resolve a requested strategy against this surface's kind.
57    /// Meaningless combos warn and fall back to the kind default
58    /// (least-opinionated: never error).
59    pub fn resolve_strategy(self, requested: Option<Strategy>) -> Strategy {
60        let req = match requested {
61            None => return self.default_strategy(),
62            Some(s) => s,
63        };
64        let ok = matches!(
65            (self.kind(), req),
66            (_, Strategy::Native)
67                | (_, Strategy::Block)
68                | (SurfaceKind::Noise, Strategy::Seeded | Strategy::Random)
69                | (SurfaceKind::Value, Strategy::Value)
70                | (SurfaceKind::Policy, Strategy::Value)
71        );
72        if ok {
73            req
74        } else {
75            tracing::warn!(
76                surface = ?self,
77                requested = ?req,
78                "strategy not meaningful for surface kind; using default"
79            );
80            self.default_strategy()
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn noise_default_is_seeded() {
91        assert_eq!(Surface::Canvas.resolve_strategy(None), Strategy::Seeded);
92    }
93
94    #[test]
95    fn value_strategy_on_noise_warns_and_falls_back() {
96        // Value is meaningless for a noise surface → falls back to Seeded.
97        assert_eq!(
98            Surface::Canvas.resolve_strategy(Some(Strategy::Value)),
99            Strategy::Seeded
100        );
101    }
102
103    #[test]
104    fn native_and_block_always_pass() {
105        assert_eq!(
106            Surface::Webgl.resolve_strategy(Some(Strategy::Native)),
107            Strategy::Native
108        );
109        assert_eq!(
110            Surface::Audio.resolve_strategy(Some(Strategy::Block)),
111            Strategy::Block
112        );
113    }
114
115    #[test]
116    fn webrtc_default_is_block() {
117        assert_eq!(Surface::Webrtc.resolve_strategy(None), Strategy::Block);
118    }
119
120    #[test]
121    fn webgpu_is_value_kind_default_value() {
122        assert_eq!(Surface::Webgpu.kind(), SurfaceKind::Value);
123        assert_eq!(Surface::Webgpu.resolve_strategy(None), Strategy::Value);
124    }
125
126    #[test]
127    fn webgpu_block_passes() {
128        assert_eq!(
129            Surface::Webgpu.resolve_strategy(Some(Strategy::Block)),
130            Strategy::Block
131        );
132    }
133}