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