script/dom/servoparser/
prefetch.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::ops::Deref;
7
8use html5ever::buffer_queue::BufferQueue;
9use html5ever::tokenizer::states::RawKind;
10use html5ever::tokenizer::{
11    Tag, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer as HtmlTokenizer,
12};
13use html5ever::{Attribute, LocalName, local_name};
14use js::jsapi::JSTracer;
15use markup5ever::TokenizerResult;
16use net_traits::policy_container::PolicyContainer;
17use net_traits::request::{
18    CorsSettings, CredentialsMode, Destination, InsecureRequestsPolicy, ParserMetadata, Referrer,
19    RequestClient,
20};
21use net_traits::{CoreResourceMsg, FetchChannels, ReferrerPolicy, ResourceThreads};
22use servo_base::generic_channel::GenericSend;
23use servo_base::id::{PipelineId, WebViewId};
24use servo_url::{ImmutableOrigin, ServoUrl};
25
26use crate::dom::bindings::reflector::DomGlobal;
27use crate::dom::bindings::trace::{CustomTraceable, JSTraceable};
28use crate::dom::document::Document;
29use crate::dom::html::htmlscriptelement::script_fetch_request;
30use crate::dom::processingoptions::determine_cors_settings_for_token;
31use crate::fetch::create_a_potential_cors_request;
32use crate::script_module::ScriptFetchOptions;
33
34#[derive(JSTraceable, MallocSizeOf)]
35#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
36pub(crate) struct Tokenizer {
37    #[ignore_malloc_size_of = "Defined in html5ever"]
38    inner: TraceableTokenizer,
39}
40
41struct TraceableTokenizer(HtmlTokenizer<PrefetchSink>);
42
43impl Deref for TraceableTokenizer {
44    type Target = HtmlTokenizer<PrefetchSink>;
45    fn deref(&self) -> &Self::Target {
46        &self.0
47    }
48}
49
50#[expect(unsafe_code)]
51unsafe impl JSTraceable for TraceableTokenizer {
52    unsafe fn trace(&self, trc: *mut JSTracer) {
53        unsafe { CustomTraceable::trace(&self.0, trc) }
54    }
55}
56
57#[expect(unsafe_code)]
58unsafe impl CustomTraceable for PrefetchSink {
59    unsafe fn trace(&self, trc: *mut JSTracer) {
60        unsafe { <Self as JSTraceable>::trace(self, trc) }
61    }
62}
63
64impl Tokenizer {
65    pub(crate) fn new(document: &Document) -> Self {
66        let global = document.global();
67        let sink = PrefetchSink {
68            origin: document.origin().immutable().clone(),
69            pipeline_id: global.pipeline_id(),
70            webview_id: document.webview_id(),
71            base_url: RefCell::new(None),
72            document_url: document.url(),
73            referrer: global.get_referrer(),
74            referrer_policy: document.get_referrer_policy(),
75            resource_threads: document.loader().resource_threads().clone(),
76            // Initially we set prefetching to false, and only set it
77            // true after the first script tag, since that is what will
78            // block the main parser.
79            prefetching: Cell::new(false),
80            insecure_requests_policy: document.insecure_requests_policy(),
81            has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(),
82            policy_container: global.policy_container(),
83            request_client: global.request_client(),
84        };
85        let options = Default::default();
86        let inner = TraceableTokenizer(HtmlTokenizer::new(sink, options));
87        Tokenizer { inner }
88    }
89
90    pub(crate) fn feed(&self, input: &BufferQueue) {
91        while let TokenizerResult::Script(PrefetchHandle) = self.inner.feed(input) {}
92    }
93}
94
95#[derive(JSTraceable)]
96struct PrefetchSink {
97    #[no_trace]
98    origin: ImmutableOrigin,
99    #[no_trace]
100    pipeline_id: PipelineId,
101    #[no_trace]
102    webview_id: WebViewId,
103    #[no_trace]
104    document_url: ServoUrl,
105    #[no_trace]
106    base_url: RefCell<Option<ServoUrl>>,
107    #[no_trace]
108    referrer: Referrer,
109    #[no_trace]
110    referrer_policy: ReferrerPolicy,
111    #[no_trace]
112    resource_threads: ResourceThreads,
113    prefetching: Cell<bool>,
114    #[no_trace]
115    insecure_requests_policy: InsecureRequestsPolicy,
116    has_trustworthy_ancestor_origin: bool,
117    #[no_trace]
118    policy_container: PolicyContainer,
119    #[no_trace]
120    request_client: RequestClient,
121}
122
123/// The prefetch tokenizer produces trivial results
124#[derive(Clone, Copy, JSTraceable)]
125struct PrefetchHandle;
126
127impl TokenSink for PrefetchSink {
128    type Handle = PrefetchHandle;
129    fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<PrefetchHandle> {
130        let tag = match token {
131            Token::TagToken(ref tag) => tag,
132            _ => return TokenSinkResult::Continue,
133        };
134        match (tag.kind, &tag.name) {
135            (TagKind::StartTag, &local_name!("script")) if self.prefetching.get() => {
136                if let Some(url) = self.get_url(tag, local_name!("src")) {
137                    debug!("Prefetch script {}", url);
138                    let cors_setting = self.get_cors_settings(tag, local_name!("crossorigin"));
139                    let integrity_metadata = self
140                        .get_attr(tag, local_name!("integrity"))
141                        .map(|attr| String::from(&attr.value))
142                        .unwrap_or_default();
143                    let cryptographic_nonce = self
144                        .get_attr(tag, local_name!("nonce"))
145                        .map(|attr| String::from(&attr.value))
146                        .unwrap_or_default();
147                    let request = script_fetch_request(
148                        self.webview_id,
149                        url,
150                        cors_setting,
151                        ScriptFetchOptions {
152                            referrer_policy: self.referrer_policy,
153                            integrity_metadata,
154                            cryptographic_nonce,
155                            credentials_mode: CredentialsMode::CredentialsSameOrigin,
156                            parser_metadata: ParserMetadata::ParserInserted,
157                            render_blocking: false,
158                        },
159                        self.referrer.clone(),
160                    )
161                    .insecure_requests_policy(self.insecure_requests_policy)
162                    .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_origin)
163                    .policy_container(self.policy_container.clone())
164                    .client(self.request_client.clone())
165                    .origin(self.origin.clone())
166                    .pipeline_id(Some(self.pipeline_id));
167                    let _ = self
168                        .resource_threads
169                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
170                }
171                TokenSinkResult::RawData(RawKind::ScriptData)
172            },
173            (TagKind::StartTag, &local_name!("img")) if self.prefetching.get() => {
174                if let Some(url) = self.get_url(tag, local_name!("src")) {
175                    debug!("Prefetch {} {}", tag.name, url);
176                    let request = create_a_potential_cors_request(
177                        Some(self.webview_id),
178                        url,
179                        Destination::Image,
180                        self.get_cors_settings(tag, local_name!("crossorigin")),
181                        None,
182                        self.referrer.clone(),
183                    )
184                    .insecure_requests_policy(self.insecure_requests_policy)
185                    .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_origin)
186                    .policy_container(self.policy_container.clone())
187                    .client(self.request_client.clone())
188                    .origin(self.origin.clone())
189                    .pipeline_id(Some(self.pipeline_id))
190                    .referrer_policy(self.get_referrer_policy(tag, local_name!("referrerpolicy")));
191
192                    let _ = self
193                        .resource_threads
194                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
195                }
196                TokenSinkResult::Continue
197            },
198            (TagKind::StartTag, &local_name!("link")) if self.prefetching.get() => {
199                if let Some(rel) = self.get_attr(tag, local_name!("rel")) {
200                    if rel.value.eq_ignore_ascii_case("stylesheet") {
201                        if let Some(url) = self.get_url(tag, local_name!("href")) {
202                            debug!("Prefetch {} {}", tag.name, url);
203                            let cors_setting =
204                                self.get_cors_settings(tag, local_name!("crossorigin"));
205                            let referrer_policy =
206                                self.get_referrer_policy(tag, local_name!("referrerpolicy"));
207                            let integrity_metadata = self
208                                .get_attr(tag, local_name!("integrity"))
209                                .map(|attr| String::from(&attr.value))
210                                .unwrap_or_default();
211
212                            // https://html.spec.whatwg.org/multipage/#default-fetch-and-process-the-linked-resource
213                            let request = create_a_potential_cors_request(
214                                Some(self.webview_id),
215                                url,
216                                Destination::Style,
217                                cors_setting,
218                                None,
219                                self.referrer.clone(),
220                            )
221                            .insecure_requests_policy(self.insecure_requests_policy)
222                            .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_origin)
223                            .policy_container(self.policy_container.clone())
224                            .client(self.request_client.clone())
225                            .origin(self.origin.clone())
226                            .pipeline_id(Some(self.pipeline_id))
227                            .referrer_policy(referrer_policy)
228                            .integrity_metadata(integrity_metadata);
229
230                            let _ = self
231                                .resource_threads
232                                .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
233                        }
234                    }
235                }
236                TokenSinkResult::Continue
237            },
238            (TagKind::StartTag, &local_name!("script")) => {
239                TokenSinkResult::RawData(RawKind::ScriptData)
240            },
241            (TagKind::EndTag, &local_name!("script")) => {
242                // After the first script tag, the main parser is blocked, so it's worth prefetching.
243                self.prefetching.set(true);
244                TokenSinkResult::Script(PrefetchHandle)
245            },
246            (TagKind::StartTag, &local_name!("base")) => {
247                if let Some(url) = self.get_url(tag, local_name!("href")) {
248                    if self.base_url.borrow().is_none() {
249                        debug!("Setting base {}", url);
250                        *self.base_url.borrow_mut() = Some(url);
251                    }
252                }
253                TokenSinkResult::Continue
254            },
255            _ => TokenSinkResult::Continue,
256        }
257    }
258}
259
260impl PrefetchSink {
261    fn get_attr<'a>(&'a self, tag: &'a Tag, name: LocalName) -> Option<&'a Attribute> {
262        tag.attrs.iter().find(|attr| attr.name.local == name)
263    }
264
265    fn get_url(&self, tag: &Tag, name: LocalName) -> Option<ServoUrl> {
266        let attr = self.get_attr(tag, name)?;
267        let base_url = self.base_url.borrow();
268        let base = base_url.as_ref().unwrap_or(&self.document_url);
269        ServoUrl::parse_with_base(Some(base), &attr.value).ok()
270    }
271
272    fn get_referrer_policy(&self, tag: &Tag, name: LocalName) -> ReferrerPolicy {
273        self.get_attr(tag, name)
274            .map(|attr| ReferrerPolicy::from(&*attr.value))
275            .unwrap_or(self.referrer_policy)
276    }
277
278    fn get_cors_settings(&self, tag: &Tag, name: LocalName) -> Option<CorsSettings> {
279        let attr = self.get_attr(tag, name)?;
280        determine_cors_settings_for_token(&attr.value)
281    }
282}