Skip to main content

termivar_scanner/payload_strategies/
sql_quote_balance_query_pair.rs

1//! Bounded, non-destructive SQL quote-balance review mutation.
2//!
3//! The candidate appends one unmatched ASCII quote to an inert scanner-owned
4//! token. It contains no comment, operator, statement separator, function,
5//! or data-bearing syntax, so its intended effect is limited to parser
6//! behavior rather than changing legitimate result selection.
7
8use crate::payload_strategy::{
9    PayloadArtifact, PayloadSeed, PayloadStrategy, PayloadStrategyError, PayloadStrategyLimits,
10    PayloadStrategyRef, PayloadVariantRole,
11};
12
13pub const SQL_QUOTE_BALANCE_QUERY_PAIR_ID: &str = "web.review.sql.quote-balance-query-pair";
14pub const SQL_QUOTE_BALANCE_QUERY_PAIR_REVISION: u32 = 1;
15
16#[derive(Debug, Clone)]
17pub struct SqlQuoteBalanceQueryPairStrategy {
18    reference: PayloadStrategyRef,
19}
20
21impl SqlQuoteBalanceQueryPairStrategy {
22    pub fn new() -> Self {
23        Self {
24            reference: PayloadStrategyRef::new(
25                SQL_QUOTE_BALANCE_QUERY_PAIR_ID,
26                SQL_QUOTE_BALANCE_QUERY_PAIR_REVISION,
27            )
28            .expect("the SQL quote-balance strategy identity is static and valid"),
29        }
30    }
31
32    fn valid_seed(seed: &[u8]) -> bool {
33        (8..=64).contains(&seed.len())
34            && seed.iter().all(|byte| {
35                byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~')
36            })
37    }
38}
39
40impl Default for SqlQuoteBalanceQueryPairStrategy {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl PayloadStrategy for SqlQuoteBalanceQueryPairStrategy {
47    fn strategy_ref(&self) -> &PayloadStrategyRef {
48        &self.reference
49    }
50
51    fn derive_one(
52        &self,
53        role: PayloadVariantRole,
54        seed: &PayloadSeed,
55        limits: PayloadStrategyLimits,
56    ) -> Result<PayloadArtifact, PayloadStrategyError> {
57        if !Self::valid_seed(seed.as_bytes()) {
58            return Err(PayloadStrategyError::DerivationFailed);
59        }
60        let mut bytes = seed.as_bytes().to_vec();
61        if role == PayloadVariantRole::Candidate {
62            bytes.push(b'\'');
63        }
64        PayloadArtifact::new(self.reference.clone(), role, bytes, limits)
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    fn limits() -> PayloadStrategyLimits {
73        PayloadStrategyLimits::new(64, 65).unwrap()
74    }
75
76    #[test]
77    fn catalog_is_exactly_one_quote_and_contains_no_sql_control_syntax() {
78        let strategy = SqlQuoteBalanceQueryPairStrategy::new();
79        let seed = PayloadSeed::new(b"venom-review-a1b2c3".to_vec(), limits()).unwrap();
80        let control = strategy
81            .derive_one(PayloadVariantRole::Control, &seed, limits())
82            .unwrap();
83        let candidate = strategy
84            .derive_one(PayloadVariantRole::Candidate, &seed, limits())
85            .unwrap();
86        assert_eq!(control.as_bytes(), b"venom-review-a1b2c3");
87        assert_eq!(candidate.as_bytes(), b"venom-review-a1b2c3'");
88        for forbidden in [b";".as_slice(), b"--", b"/*", b"=", b"(", b")"] {
89            assert!(!candidate
90                .as_bytes()
91                .windows(forbidden.len())
92                .any(|w| w == forbidden));
93        }
94    }
95
96    #[test]
97    fn unsafe_or_ambiguous_seeds_fail_closed() {
98        let strategy = SqlQuoteBalanceQueryPairStrategy::new();
99        for value in [
100            b"short".as_slice(),
101            b"contains space",
102            b"contains'quote",
103            b"contains;statement",
104            b"contains=operator",
105        ] {
106            let seed = PayloadSeed::new(value.to_vec(), limits()).unwrap();
107            for role in [PayloadVariantRole::Control, PayloadVariantRole::Candidate] {
108                assert!(matches!(
109                    strategy.derive_one(role, &seed, limits()),
110                    Err(PayloadStrategyError::DerivationFailed)
111                ));
112            }
113        }
114    }
115
116    #[test]
117    fn repeat_is_byte_deterministic_and_bounded() {
118        let strategy = SqlQuoteBalanceQueryPairStrategy::new();
119        let seed = PayloadSeed::new(b"venom-review-repeat".to_vec(), limits()).unwrap();
120        let first = strategy
121            .derive_one(PayloadVariantRole::Candidate, &seed, limits())
122            .unwrap();
123        let second = strategy
124            .derive_one(PayloadVariantRole::Candidate, &seed, limits())
125            .unwrap();
126        assert_eq!(first, second);
127        assert_eq!(first.strategy(), strategy.strategy_ref());
128    }
129}