Skip to main content

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