Skip to main content

blitz_dom/
net.rs

1use selectors::context::QuirksMode;
2use std::sync::atomic::Ordering as Ao;
3use std::{
4    io::Cursor,
5    sync::{Arc, atomic::AtomicUsize, mpsc::Sender},
6};
7use style::{
8    font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, Source},
9    media_queries::MediaList,
10    servo_arc::Arc as ServoArc,
11    shared_lock::SharedRwLock,
12    shared_lock::{Locked, SharedRwLockReadGuard},
13    stylesheets::{
14        AllowImportRules, CssRule, DocumentStyleSheet, ImportRule, Origin, Stylesheet,
15        StylesheetInDocument, StylesheetLoader as ServoStylesheetLoader, UrlExtraData,
16        import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition},
17    },
18    values::{CssUrl, SourceLocation},
19};
20
21use blitz_traits::net::{Bytes, NetHandler, NetProvider, Request};
22use blitz_traits::shell::ShellProvider;
23
24use url::Url;
25
26use crate::{document::DocumentEvent, util::ImageType};
27
28#[derive(Clone, Debug)]
29pub enum Resource {
30    Image(ImageType, u32, u32, Arc<Vec<u8>>),
31    #[cfg(feature = "svg")]
32    Svg(ImageType, Arc<usvg::Tree>),
33    Css(DocumentStyleSheet),
34    Font(Bytes),
35    None,
36}
37
38pub(crate) struct ResourceHandler<T: Send + Sync + 'static> {
39    doc_id: usize,
40    request_id: usize,
41    node_id: Option<usize>,
42    tx: Sender<DocumentEvent>,
43    shell_provider: Arc<dyn ShellProvider>,
44    data: T,
45}
46
47impl<T: Send + Sync + 'static> ResourceHandler<T> {
48    pub(crate) fn new(
49        tx: Sender<DocumentEvent>,
50        doc_id: usize,
51        node_id: Option<usize>,
52        shell_provider: Arc<dyn ShellProvider>,
53        data: T,
54    ) -> Self {
55        static REQUEST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
56        Self {
57            request_id: REQUEST_ID_COUNTER.fetch_add(1, Ao::Relaxed),
58            doc_id,
59            node_id,
60            tx,
61            shell_provider,
62            data,
63        }
64    }
65
66    pub(crate) fn boxed(
67        tx: Sender<DocumentEvent>,
68        doc_id: usize,
69        node_id: Option<usize>,
70        shell_provider: Arc<dyn ShellProvider>,
71        data: T,
72    ) -> Box<dyn NetHandler>
73    where
74        ResourceHandler<T>: NetHandler,
75    {
76        Box::new(Self::new(tx, doc_id, node_id, shell_provider, data)) as _
77    }
78
79    pub(crate) fn request_id(&self) -> usize {
80        self.request_id
81    }
82
83    fn respond(&self, resolved_url: String, result: Result<Resource, String>) {
84        let response = ResourceLoadResponse {
85            request_id: self.request_id,
86            node_id: self.node_id,
87            resolved_url: Some(resolved_url),
88            result,
89        };
90        let _ = self.tx.send(DocumentEvent::ResourceLoad(response));
91        self.shell_provider.request_redraw();
92    }
93}
94
95#[allow(unused)]
96pub struct ResourceLoadResponse {
97    pub request_id: usize,
98    pub node_id: Option<usize>,
99    pub resolved_url: Option<String>,
100    pub result: Result<Resource, String>,
101}
102
103pub struct StylesheetHandler {
104    pub source_url: Url,
105    pub guard: SharedRwLock,
106    pub net_provider: Arc<dyn NetProvider>,
107}
108
109impl NetHandler for ResourceHandler<StylesheetHandler> {
110    fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
111        let Ok(css) = std::str::from_utf8(&bytes) else {
112            return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
113        };
114
115        // NOTE(Nico): I don't *think* external stylesheets should have HTML entities escaped
116        // let escaped_css = html_escape::decode_html_entities(css);
117
118        let sheet = Stylesheet::from_str(
119            css,
120            self.data.source_url.clone().into(),
121            Origin::Author,
122            ServoArc::new(self.data.guard.wrap(MediaList::empty())),
123            self.data.guard.clone(),
124            Some(&StylesheetLoader {
125                tx: self.tx.clone(),
126                doc_id: self.doc_id,
127                net_provider: self.data.net_provider.clone(),
128                shell_provider: self.shell_provider.clone(),
129            }),
130            None, // error_reporter
131            QuirksMode::NoQuirks,
132            AllowImportRules::Yes,
133        );
134
135        self.respond(
136            resolved_url,
137            Ok(Resource::Css(DocumentStyleSheet(ServoArc::new(sheet)))),
138        );
139    }
140}
141
142#[derive(Clone)]
143pub(crate) struct StylesheetLoader {
144    pub(crate) tx: Sender<DocumentEvent>,
145    pub(crate) doc_id: usize,
146    pub(crate) net_provider: Arc<dyn NetProvider>,
147    pub(crate) shell_provider: Arc<dyn ShellProvider>,
148}
149impl ServoStylesheetLoader for StylesheetLoader {
150    fn request_stylesheet(
151        &self,
152        url: CssUrl,
153        location: SourceLocation,
154        lock: &SharedRwLock,
155        media: ServoArc<Locked<MediaList>>,
156        supports: Option<ImportSupportsCondition>,
157        layer: ImportLayer,
158    ) -> ServoArc<Locked<ImportRule>> {
159        if !supports.as_ref().is_none_or(|s| s.enabled) {
160            return ServoArc::new(lock.wrap(ImportRule {
161                url,
162                stylesheet: ImportSheet::new_refused(),
163                supports,
164                layer,
165                source_location: location,
166            }));
167        }
168
169        let import = ImportRule {
170            url,
171            stylesheet: ImportSheet::new_pending(),
172            supports,
173            layer,
174            source_location: location,
175        };
176
177        let url = import.url.url().unwrap().clone();
178        let import = ServoArc::new(lock.wrap(import));
179        self.net_provider.fetch(
180            self.doc_id,
181            Request::get(url.as_ref().clone()),
182            ResourceHandler::boxed(
183                self.tx.clone(),
184                self.doc_id,
185                None, // node_id
186                self.shell_provider.clone(),
187                NestedStylesheetHandler {
188                    url: url.clone(),
189                    loader: self.clone(),
190                    lock: lock.clone(),
191                    media,
192                    import_rule: import.clone(),
193                    net_provider: self.net_provider.clone(),
194                },
195            ),
196        );
197
198        import
199    }
200}
201
202struct NestedStylesheetHandler {
203    loader: StylesheetLoader,
204    lock: SharedRwLock,
205    url: ServoArc<Url>,
206    media: ServoArc<Locked<MediaList>>,
207    import_rule: ServoArc<Locked<ImportRule>>,
208    net_provider: Arc<dyn NetProvider>,
209}
210
211impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
212    fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
213        let Ok(css) = std::str::from_utf8(&bytes) else {
214            return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
215        };
216
217        // NOTE(Nico): I don't *think* external stylesheets should have HTML entities escaped
218        // let escaped_css = html_escape::decode_html_entities(css);
219
220        let sheet = ServoArc::new(Stylesheet::from_str(
221            css,
222            UrlExtraData(self.data.url.clone()),
223            Origin::Author,
224            self.data.media.clone(),
225            self.data.lock.clone(),
226            Some(&self.data.loader),
227            None, // error_reporter
228            QuirksMode::NoQuirks,
229            AllowImportRules::Yes,
230        ));
231
232        // Fetch @font-face fonts
233        fetch_font_face(
234            self.tx.clone(),
235            self.doc_id,
236            self.node_id,
237            &sheet,
238            &self.data.net_provider,
239            &self.shell_provider,
240            &self.data.lock.read(),
241        );
242
243        let mut guard = self.data.lock.write();
244        self.data.import_rule.write_with(&mut guard).stylesheet = ImportSheet::Sheet(sheet);
245        drop(guard);
246
247        self.respond(resolved_url, Ok(Resource::None))
248    }
249}
250
251struct FontFaceHandler(FontFaceSourceFormatKeyword);
252impl NetHandler for ResourceHandler<FontFaceHandler> {
253    fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
254        let result = self.data.parse(bytes);
255        self.respond(resolved_url, result)
256    }
257}
258impl FontFaceHandler {
259    fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
260        if self.0 == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
261            self.0 = match &bytes.as_ref()[0..4] {
262                // WOFF (v1) files begin with 0x774F4646 ('wOFF' in ascii)
263                // See: <https://w3c.github.io/woff/woff1/spec/Overview.html#WOFFHeader>
264                #[cfg(feature = "woff")]
265                b"wOFF" => FontFaceSourceFormatKeyword::Woff,
266                // WOFF2 files begin with 0x774F4632 ('wOF2' in ascii)
267                // See: <https://w3c.github.io/woff/woff2/#woff20Header>
268                #[cfg(feature = "woff")]
269                b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
270                // Opentype fonts with CFF data begin with 0x4F54544F ('OTTO' in ascii)
271                // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font>
272                b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
273                // Opentype fonts truetype outlines begin with 0x00010000
274                // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font>
275                &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
276                // Truetype fonts begin with 0x74727565 ('true' in ascii)
277                // See: <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html#ScalerTypeNote>
278                b"true" => FontFaceSourceFormatKeyword::Truetype,
279                _ => FontFaceSourceFormatKeyword::None,
280            }
281        }
282
283        // Satisfy rustc's mutability linting with woff feature both enabled/disabled
284        #[cfg(feature = "woff")]
285        let mut bytes = bytes;
286
287        match self.0 {
288            #[cfg(feature = "woff")]
289            FontFaceSourceFormatKeyword::Woff => {
290                #[cfg(feature = "tracing")]
291                tracing::info!("Decompressing woff1 font");
292
293                // Use wuff crate to decompress font
294                let decompressed = wuff::decompress_woff1(&bytes).ok();
295
296                if let Some(decompressed) = decompressed {
297                    bytes = Bytes::from(decompressed);
298                } else {
299                    #[cfg(feature = "tracing")]
300                    tracing::warn!("Failed to decompress woff1 font");
301                }
302            }
303            #[cfg(feature = "woff")]
304            FontFaceSourceFormatKeyword::Woff2 => {
305                #[cfg(feature = "tracing")]
306                tracing::info!("Decompressing woff2 font");
307
308                // Use wuff crate to decompress font
309                let decompressed = wuff::decompress_woff2(&bytes).ok();
310
311                if let Some(decompressed) = decompressed {
312                    bytes = Bytes::from(decompressed);
313                } else {
314                    #[cfg(feature = "tracing")]
315                    tracing::warn!("Failed to decompress woff2 font");
316                }
317            }
318            FontFaceSourceFormatKeyword::None => {
319                // Should this be an error?
320                return Ok(Resource::None);
321            }
322            _ => {}
323        }
324
325        Ok(Resource::Font(bytes))
326    }
327}
328
329pub(crate) fn fetch_font_face(
330    tx: Sender<DocumentEvent>,
331    doc_id: usize,
332    node_id: Option<usize>,
333    sheet: &Stylesheet,
334    network_provider: &Arc<dyn NetProvider>,
335    shell_provider: &Arc<dyn ShellProvider>,
336    read_guard: &SharedRwLockReadGuard,
337) {
338    sheet
339        .contents(read_guard)
340        .rules(read_guard)
341        .iter()
342        .filter_map(|rule| match rule {
343            CssRule::FontFace(font_face) => {
344                // Return source list if both source list and font_family are present
345                let descriptor = &font_face.read_with(read_guard).descriptors;
346                descriptor
347                    .src
348                    .as_ref()
349                    .filter(|_| descriptor.font_family.is_some())
350            }
351            _ => None,
352        })
353        .for_each(|source_list| {
354            // Find the first font source in the source list that specifies a font of a type
355            // that we support.
356            let preferred_source = source_list
357                .0
358                .iter()
359                .filter_map(|source| match source {
360                    Source::Url(url_source) => Some(url_source),
361                    // TODO: support local fonts in @font-face
362                    Source::Local(_) => None,
363                })
364                .find_map(|url_source| {
365                    let mut format = match &url_source.format_hint {
366                        Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
367                        Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
368                            "woff2" => FontFaceSourceFormatKeyword::Woff2,
369                            "ttf" => FontFaceSourceFormatKeyword::Truetype,
370                            "otf" => FontFaceSourceFormatKeyword::Opentype,
371                            _ => FontFaceSourceFormatKeyword::None,
372                        },
373                        _ => FontFaceSourceFormatKeyword::None,
374                    };
375                    if format == FontFaceSourceFormatKeyword::None {
376                        let (_, end) = url_source.url.as_str().rsplit_once('.')?;
377                        format = match end {
378                            "woff2" => FontFaceSourceFormatKeyword::Woff2,
379                            "woff" => FontFaceSourceFormatKeyword::Woff,
380                            "ttf" => FontFaceSourceFormatKeyword::Truetype,
381                            "otf" => FontFaceSourceFormatKeyword::Opentype,
382                            "svg" => FontFaceSourceFormatKeyword::Svg,
383                            "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
384                            _ => FontFaceSourceFormatKeyword::None,
385                        }
386                    }
387
388                    if matches!(
389                        format,
390                        FontFaceSourceFormatKeyword::Svg
391                            | FontFaceSourceFormatKeyword::EmbeddedOpentype
392                    ) {
393                        #[cfg(feature = "tracing")]
394                        tracing::warn!("Skipping unsupported font of type {:?}", format);
395                        return None;
396                    }
397
398                    #[cfg(not(feature = "woff"))]
399                    if matches!(
400                        format,
401                        FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
402                    ) {
403                        #[cfg(feature = "tracing")]
404                        tracing::warn!("Skipping unsupported font of type {:?}", format);
405                        return None;
406                    }
407
408                    let url = url_source.url.url().unwrap().as_ref().clone();
409                    Some((url, format))
410                });
411
412            if let Some((url, format)) = preferred_source {
413                network_provider.fetch(
414                    doc_id,
415                    Request::get(url),
416                    ResourceHandler::boxed(
417                        tx.clone(),
418                        doc_id,
419                        node_id,
420                        shell_provider.clone(),
421                        FontFaceHandler(format),
422                    ),
423                );
424            }
425        })
426}
427
428pub struct ImageHandler {
429    kind: ImageType,
430}
431impl ImageHandler {
432    pub fn new(kind: ImageType) -> Self {
433        Self { kind }
434    }
435}
436
437impl NetHandler for ResourceHandler<ImageHandler> {
438    fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
439        let result = self.data.parse(bytes);
440        self.respond(resolved_url, result)
441    }
442}
443
444impl ImageHandler {
445    fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
446        // Try parse image
447        if let Ok(image) = image::ImageReader::new(Cursor::new(&bytes))
448            .with_guessed_format()
449            .expect("IO errors impossible with Cursor")
450            .decode()
451        {
452            let raw_rgba8_data = image.clone().into_rgba8().into_raw();
453            return Ok(Resource::Image(
454                self.kind,
455                image.width(),
456                image.height(),
457                Arc::new(raw_rgba8_data),
458            ));
459        };
460
461        #[cfg(feature = "svg")]
462        {
463            use crate::util::parse_svg;
464            if let Ok(tree) = parse_svg(&bytes) {
465                return Ok(Resource::Svg(self.kind, Arc::new(tree)));
466            }
467        }
468
469        Err(String::from("Could not parse image"))
470    }
471}