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}