Skip to main content

zendriver_interception/
rule.rs

1//! Declarative interception rules.
2//!
3//! Each rule pairs a [`UrlPattern`] with one of four actions ([`Block`],
4//! [`Redirect`], [`Respond`], [`Modify`]). The actor in T6 walks the rule list
5//! in registration order on each `Fetch.requestPaused` event; the first rule
6//! whose pattern matches the request URL wins.
7//!
8//! [`Block`]: Rule::Block
9//! [`Redirect`]: Rule::Redirect
10//! [`Respond`]: Rule::Respond
11//! [`Modify`]: Rule::Modify
12
13use std::fmt;
14use std::sync::Arc;
15
16use crate::types::{RequestInfo, RequestOverrides};
17use crate::url_pattern::UrlPattern;
18
19/// A single interception rule.
20///
21/// Each variant carries its own [`UrlPattern`], so different rules in the same
22/// [`InterceptBuilder`](crate::builder::InterceptBuilder) can match disjoint
23/// URL sets. Rules are evaluated in registration order — earlier rules
24/// shadow later ones for overlapping patterns.
25//
26// Not `Clone`: `Modify` holds an `Arc<dyn Fn ...>` which is cheap to clone,
27// but the other variants own `Vec`s/`String`s that we'd rather not silently
28// duplicate. The actor consumes the rule list by reference (`&Rule`) so a
29// `Clone` impl is unnecessary in practice.
30pub enum Rule {
31    /// Abort matching requests with `Fetch.failRequest`
32    /// (`errorReason: "BlockedByClient"`).
33    Block {
34        /// URL pattern matched against `Fetch.requestPaused.request.url`.
35        pattern: UrlPattern,
36    },
37    /// Redirect matching requests to `to` via `Fetch.continueRequest { url }`.
38    Redirect {
39        /// URL pattern matched against the incoming request URL.
40        from: UrlPattern,
41        /// Absolute target URL substituted into the continued request.
42        to: String,
43    },
44    /// Serve a synthesized response with `Fetch.fulfillRequest`.
45    Respond {
46        /// URL pattern matched against the incoming request URL.
47        pattern: UrlPattern,
48        /// HTTP status code returned to the page (`responseCode`).
49        status: u16,
50        /// Response headers as `(name, value)` pairs.
51        headers: Vec<(String, String)>,
52        /// Raw response body bytes (base64-encoded on the wire by the actor).
53        body: Vec<u8>,
54    },
55    /// Rewrite the outgoing request per-field via a user closure, then
56    /// continue. The closure receives the live [`RequestInfo`] and returns
57    /// the [`RequestOverrides`] to apply.
58    Modify {
59        /// URL pattern matched against the incoming request URL.
60        pattern: UrlPattern,
61        /// Closure invoked per matching request to produce overrides.
62        ///
63        /// Wrapped in [`Arc`] so the actor can cheaply share the closure
64        /// across the rule list without forcing the rule itself to be `Clone`
65        /// or `Send`-by-value.
66        modify: Arc<dyn Fn(&RequestInfo) -> RequestOverrides + Send + Sync>,
67    },
68}
69
70impl Rule {
71    /// Test whether this rule's pattern matches `url`.
72    ///
73    /// Delegates to the embedded [`UrlPattern::matches`]; the field selector
74    /// (`pattern`, `from`) varies per variant but the semantics are identical
75    /// — a CDP-style wildcard match against the full request URL.
76    pub fn matches(&self, url: &str) -> bool {
77        match self {
78            Self::Block { pattern }
79            | Self::Respond { pattern, .. }
80            | Self::Modify { pattern, .. } => pattern.matches(url),
81            Self::Redirect { from, .. } => from.matches(url),
82        }
83    }
84}
85
86// Hand-written `Debug` because the `Modify` variant holds `Arc<dyn Fn ...>`,
87// which is not `Debug`. We print the closure as a placeholder so the rest of
88// the rule (the pattern) stays inspectable.
89impl fmt::Debug for Rule {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::Block { pattern } => f.debug_struct("Block").field("pattern", pattern).finish(),
93            Self::Redirect { from, to } => f
94                .debug_struct("Redirect")
95                .field("from", from)
96                .field("to", to)
97                .finish(),
98            Self::Respond {
99                pattern,
100                status,
101                headers,
102                body,
103            } => f
104                .debug_struct("Respond")
105                .field("pattern", pattern)
106                .field("status", status)
107                .field("headers", headers)
108                .field("body_len", &body.len())
109                .finish(),
110            Self::Modify { pattern, .. } => f
111                .debug_struct("Modify")
112                .field("pattern", pattern)
113                .field("modify", &"<closure>")
114                .finish(),
115        }
116    }
117}
118
119#[cfg(test)]
120#[allow(clippy::panic, clippy::unwrap_used)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn block_matches_via_pattern() {
126        let rule = Rule::Block {
127            pattern: UrlPattern::new("*/ads/*").unwrap(),
128        };
129        assert!(rule.matches("https://example.com/ads/banner.png"));
130        assert!(!rule.matches("https://example.com/content/main.css"));
131    }
132
133    #[test]
134    fn redirect_matches_via_from_field() {
135        let rule = Rule::Redirect {
136            from: UrlPattern::new("*/old/*").unwrap(),
137            to: "https://example.com/new/replacement".into(),
138        };
139        assert!(rule.matches("https://example.com/old/page.html"));
140        assert!(!rule.matches("https://example.com/new/page.html"));
141    }
142}