Skip to main content

zendriver_interception/
rule.rs

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