Skip to main content

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