termivar_scanner/payload_strategies/
reflection_marker_query_pair.rs1use crate::payload_strategy::{
8 PayloadArtifact, PayloadSeed, PayloadStrategy, PayloadStrategyError, PayloadStrategyLimits,
9 PayloadStrategyRef, PayloadVariantRole,
10};
11
12pub const REFLECTION_MARKER_QUERY_PAIR_ID: &str = "web.review.reflection.marker-query-pair";
13pub const REFLECTION_MARKER_QUERY_PAIR_REVISION: u32 = 1;
14
15#[derive(Debug, Clone)]
16pub struct ReflectionMarkerQueryPairStrategy {
17 reference: PayloadStrategyRef,
18}
19
20impl ReflectionMarkerQueryPairStrategy {
21 pub fn new() -> Self {
22 Self {
23 reference: PayloadStrategyRef::new(
24 REFLECTION_MARKER_QUERY_PAIR_ID,
25 REFLECTION_MARKER_QUERY_PAIR_REVISION,
26 )
27 .expect("the reflection marker strategy identity is static and valid"),
28 }
29 }
30
31 fn marker(seed: &[u8], role: PayloadVariantRole) -> Option<String> {
32 let identity = std::str::from_utf8(seed).ok()?;
33 if identity.len() != 32
34 || !identity
35 .bytes()
36 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
37 {
38 return None;
39 }
40 let leg = match role {
41 PayloadVariantRole::Control => "control",
42 PayloadVariantRole::Candidate => "candidate",
43 };
44 Some(format!("venom-reflection-{leg}-{identity}-end"))
45 }
46}
47
48impl Default for ReflectionMarkerQueryPairStrategy {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl PayloadStrategy for ReflectionMarkerQueryPairStrategy {
55 fn strategy_ref(&self) -> &PayloadStrategyRef {
56 &self.reference
57 }
58
59 fn derive_one(
60 &self,
61 role: PayloadVariantRole,
62 seed: &PayloadSeed,
63 limits: PayloadStrategyLimits,
64 ) -> Result<PayloadArtifact, PayloadStrategyError> {
65 let value =
66 Self::marker(seed.as_bytes(), role).ok_or(PayloadStrategyError::DerivationFailed)?;
67 PayloadArtifact::new(self.reference.clone(), role, value.into_bytes(), limits)
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 fn limits() -> PayloadStrategyLimits {
76 PayloadStrategyLimits::new(128, 128).unwrap()
77 }
78
79 #[test]
80 fn pair_is_distinct_inert_bounded_and_versioned() {
81 let strategy = ReflectionMarkerQueryPairStrategy::new();
82 let seed =
83 PayloadSeed::new(b"0123456789abcdef0123456789abcdef".to_vec(), limits()).unwrap();
84 let control = strategy
85 .derive_one(PayloadVariantRole::Control, &seed, limits())
86 .unwrap();
87 let candidate = strategy
88 .derive_one(PayloadVariantRole::Candidate, &seed, limits())
89 .unwrap();
90 assert_eq!(
91 strategy.strategy_ref().to_string(),
92 "web.review.reflection.marker-query-pair@1"
93 );
94 assert_ne!(control.as_bytes(), candidate.as_bytes());
95 for value in [control.as_bytes(), candidate.as_bytes()] {
96 assert!(value.len() <= 80);
97 assert!(value
98 .iter()
99 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-'));
100 }
101 }
102
103 #[test]
104 fn unbounded_or_noncanonical_seed_fails_closed() {
105 let strategy = ReflectionMarkerQueryPairStrategy::new();
106 for value in [
107 "short",
108 "0123456789ABCDEF0123456789ABCDEF",
109 "0123456789abcdef0123456789abcdeg",
110 "0123456789abcdef0123456789abcdef0",
111 ] {
112 let seed = PayloadSeed::new(value.as_bytes().to_vec(), limits()).unwrap();
113 assert!(matches!(
114 strategy.derive_one(PayloadVariantRole::Candidate, &seed, limits()),
115 Err(PayloadStrategyError::DerivationFailed)
116 ));
117 }
118 }
119}