blitz_dom/
net.rs

1use selectors::context::QuirksMode;
2use std::{io::Cursor, sync::Arc, sync::atomic::AtomicBool};
3use style::{
4    font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, Source},
5    media_queries::MediaList,
6    parser::ParserContext,
7    servo_arc::Arc as ServoArc,
8    shared_lock::SharedRwLock,
9    shared_lock::{Locked, SharedRwLockReadGuard},
10    stylesheets::{
11        AllowImportRules, CssRule, CssRules, DocumentStyleSheet, ImportRule, Origin, Stylesheet,
12        StylesheetContents, StylesheetInDocument, StylesheetLoader as ServoStylesheetLoader,
13        UrlExtraData,
14        import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition},
15    },
16    values::{CssUrl, SourceLocation},
17};
18
19use blitz_traits::net::{Bytes, NetHandler, Request, SharedCallback, SharedProvider};
20
21use url::Url;
22
23use crate::util::ImageType;
24
25#[derive(Clone, Debug)]
26pub enum Resource {
27    Image(usize, ImageType, u32, u32, Arc<Vec<u8>>),
28    #[cfg(feature = "svg")]
29    Svg(usize, ImageType, Box<usvg::Tree>),
30    Css(usize, DocumentStyleSheet),
31    Font(Bytes),
32    Navigation {
33        url: String,
34        document: Bytes,
35    },
36    None,
37}
38pub struct CssHandler {
39    pub node: usize,
40    pub source_url: Url,
41    pub guard: SharedRwLock,
42    pub provider: SharedProvider<Resource>,
43}
44
45#[derive(Clone)]
46pub(crate) struct StylesheetLoader(pub(crate) usize, pub(crate) SharedProvider<Resource>);
47impl ServoStylesheetLoader for StylesheetLoader {
48    fn request_stylesheet(
49        &self,
50        url: CssUrl,
51        location: SourceLocation,
52        context: &ParserContext,
53        lock: &SharedRwLock,
54        media: ServoArc<Locked<MediaList>>,
55        supports: Option<ImportSupportsCondition>,
56        layer: ImportLayer,
57    ) -> ServoArc<Locked<ImportRule>> {
58        if !supports.as_ref().is_none_or(|s| s.enabled) {
59            return ServoArc::new(lock.wrap(ImportRule {
60                url,
61                stylesheet: ImportSheet::new_refused(),
62                supports,
63                layer,
64                source_location: location,
65            }));
66        }
67
68        let sheet = ServoArc::new(Stylesheet {
69            contents: StylesheetContents::from_data(
70                CssRules::new(Vec::new(), lock),
71                context.stylesheet_origin,
72                context.url_data.clone(),
73                context.quirks_mode,
74            ),
75            media,
76            shared_lock: lock.clone(),
77            disabled: AtomicBool::new(false),
78        });
79
80        let stylesheet = ImportSheet::new(sheet.clone());
81        let import = ImportRule {
82            url,
83            stylesheet,
84            supports,
85            layer,
86            source_location: location,
87        };
88
89        struct StylesheetLoaderInner {
90            loader: StylesheetLoader,
91            read_lock: SharedRwLock,
92            url: ServoArc<Url>,
93            sheet: ServoArc<Stylesheet>,
94            provider: SharedProvider<Resource>,
95        }
96        impl NetHandler<Resource> for StylesheetLoaderInner {
97            fn bytes(
98                self: Box<Self>,
99                doc_id: usize,
100                bytes: Bytes,
101                callback: SharedCallback<Resource>,
102            ) {
103                let Ok(css) = std::str::from_utf8(&bytes) else {
104                    callback.call(doc_id, Err(Some(String::from("Invalid UTF8"))));
105                    return;
106                };
107
108                // NOTE(Nico): I don't *think* external stylesheets should have HTML entities escaped
109                // let escaped_css = html_escape::decode_html_entities(css);
110                Stylesheet::update_from_str(
111                    &self.sheet,
112                    css,
113                    UrlExtraData(self.url),
114                    Some(&self.loader),
115                    None,
116                    AllowImportRules::Yes,
117                );
118                fetch_font_face(doc_id, &self.sheet, &self.provider, &self.read_lock.read());
119                callback.call(doc_id, Ok(Resource::None))
120            }
121        }
122        let url = import.url.url().unwrap();
123        self.1.fetch(
124            self.0,
125            Request::get(url.as_ref().clone()),
126            Box::new(StylesheetLoaderInner {
127                url: url.clone(),
128                loader: self.clone(),
129                read_lock: lock.clone(),
130                sheet: sheet.clone(),
131                provider: self.1.clone(),
132            }),
133        );
134
135        ServoArc::new(lock.wrap(import))
136    }
137}
138impl NetHandler<Resource> for CssHandler {
139    fn bytes(self: Box<Self>, doc_id: usize, bytes: Bytes, callback: SharedCallback<Resource>) {
140        let Ok(css) = std::str::from_utf8(&bytes) else {
141            callback.call(doc_id, Err(Some(String::from("Invalid UTF8"))));
142            return;
143        };
144
145        // NOTE(Nico): I don't *think* external stylesheets should have HTML entities escaped
146        // let escaped_css = html_escape::decode_html_entities(css);
147
148        let sheet = Stylesheet::from_str(
149            css,
150            self.source_url.into(),
151            Origin::Author,
152            ServoArc::new(self.guard.wrap(MediaList::empty())),
153            self.guard.clone(),
154            Some(&StylesheetLoader(doc_id, self.provider.clone())),
155            None,
156            QuirksMode::NoQuirks,
157            AllowImportRules::Yes,
158        );
159        let read_guard = self.guard.read();
160        fetch_font_face(doc_id, &sheet, &self.provider, &read_guard);
161
162        callback.call(
163            doc_id,
164            Ok(Resource::Css(
165                self.node,
166                DocumentStyleSheet(ServoArc::new(sheet)),
167            )),
168        )
169    }
170}
171struct FontFaceHandler(FontFaceSourceFormatKeyword);
172impl NetHandler<Resource> for FontFaceHandler {
173    fn bytes(mut self: Box<Self>, doc_id: usize, bytes: Bytes, callback: SharedCallback<Resource>) {
174        if self.0 == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
175            self.0 = match &bytes.as_ref()[0..4] {
176                // WOFF (v1) files begin with 0x774F4646 ('wOFF' in ascii)
177                // See: <https://w3c.github.io/woff/woff1/spec/Overview.html#WOFFHeader>
178                #[cfg(any(feature = "woff-c", feature = "woff-rust"))]
179                b"wOFF" => FontFaceSourceFormatKeyword::Woff,
180                // WOFF2 files begin with 0x774F4632 ('wOF2' in ascii)
181                // See: <https://w3c.github.io/woff/woff2/#woff20Header>
182                #[cfg(any(feature = "woff-c", feature = "woff-rust"))]
183                b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
184                // Opentype fonts with CFF data begin with 0x4F54544F ('OTTO' in ascii)
185                // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font>
186                b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
187                // Opentype fonts truetype outlines begin with 0x00010000
188                // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font>
189                &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
190                // Truetype fonts begin with 0x74727565 ('true' in ascii)
191                // See: <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html#ScalerTypeNote>
192                b"true" => FontFaceSourceFormatKeyword::Truetype,
193                _ => FontFaceSourceFormatKeyword::None,
194            }
195        }
196
197        // Satisfy rustc's mutability linting with woff feature both enabled/disabled
198        #[cfg(any(feature = "woff-c", feature = "woff-rust"))]
199        let mut bytes = bytes;
200
201        match self.0 {
202            #[cfg(any(feature = "woff-c", feature = "woff-rust"))]
203            FontFaceSourceFormatKeyword::Woff => {
204                #[cfg(feature = "tracing")]
205                tracing::info!("Decompressing woff1 font");
206
207                // Use woff crate to decompress font
208                #[cfg(feature = "woff-c")]
209                let decompressed = woff::version1::decompress(&bytes);
210
211                // Use wuff crate to decompress font
212                #[cfg(feature = "woff-rust")]
213                let decompressed = wuff::decompress_woff1(&bytes).ok();
214
215                if let Some(decompressed) = decompressed {
216                    bytes = Bytes::from(decompressed);
217                } else {
218                    #[cfg(feature = "tracing")]
219                    tracing::warn!("Failed to decompress woff1 font");
220                }
221            }
222            #[cfg(any(feature = "woff-c", feature = "woff-rust"))]
223            FontFaceSourceFormatKeyword::Woff2 => {
224                #[cfg(feature = "tracing")]
225                tracing::info!("Decompressing woff2 font");
226
227                // Use woff crate to decompress font
228                #[cfg(feature = "woff-c")]
229                let decompressed = woff::version2::decompress(&bytes);
230
231                // Use wuff crate to decompress font
232                #[cfg(feature = "woff-rust")]
233                let decompressed = wuff::decompress_woff2(&bytes).ok();
234
235                if let Some(decompressed) = decompressed {
236                    bytes = Bytes::from(decompressed);
237                } else {
238                    #[cfg(feature = "tracing")]
239                    tracing::warn!("Failed to decompress woff2 font");
240                }
241            }
242            FontFaceSourceFormatKeyword::None => {
243                return;
244            }
245            _ => {}
246        }
247
248        callback.call(doc_id, Ok(Resource::Font(bytes)))
249    }
250}
251
252fn fetch_font_face(
253    doc_id: usize,
254    sheet: &Stylesheet,
255    network_provider: &SharedProvider<Resource>,
256    read_guard: &SharedRwLockReadGuard,
257) {
258    sheet
259        .rules(read_guard)
260        .iter()
261        .filter_map(|rule| match rule {
262            CssRule::FontFace(font_face) => font_face.read_with(read_guard).sources.as_ref(),
263            _ => None,
264        })
265        .flat_map(|source_list| &source_list.0)
266        .filter_map(|source| match source {
267            Source::Url(url_source) => Some(url_source),
268            _ => None,
269        })
270        .for_each(|url_source| {
271            let mut format = match &url_source.format_hint {
272                Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
273                Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
274                    "woff2" => FontFaceSourceFormatKeyword::Woff2,
275                    "ttf" => FontFaceSourceFormatKeyword::Truetype,
276                    "otf" => FontFaceSourceFormatKeyword::Opentype,
277                    _ => FontFaceSourceFormatKeyword::None,
278                },
279                _ => FontFaceSourceFormatKeyword::None,
280            };
281            if format == FontFaceSourceFormatKeyword::None {
282                let Some((_, end)) = url_source.url.as_str().rsplit_once('.') else {
283                    return;
284                };
285                format = match end {
286                    "woff2" => FontFaceSourceFormatKeyword::Woff2,
287                    "woff" => FontFaceSourceFormatKeyword::Woff,
288                    "ttf" => FontFaceSourceFormatKeyword::Truetype,
289                    "otf" => FontFaceSourceFormatKeyword::Opentype,
290                    "svg" => FontFaceSourceFormatKeyword::Svg,
291                    "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
292                    _ => FontFaceSourceFormatKeyword::None,
293                }
294            }
295            if let _font_format @ (FontFaceSourceFormatKeyword::Svg
296            | FontFaceSourceFormatKeyword::EmbeddedOpentype
297            | FontFaceSourceFormatKeyword::Woff) = format
298            {
299                #[cfg(feature = "tracing")]
300                tracing::warn!("Skipping unsupported font of type {:?}", _font_format);
301                return;
302            }
303            let url = url_source.url.url().unwrap().as_ref().clone();
304            network_provider.fetch(doc_id, Request::get(url), Box::new(FontFaceHandler(format)))
305        });
306}
307
308pub struct ImageHandler(usize, ImageType);
309impl ImageHandler {
310    pub fn new(node_id: usize, kind: ImageType) -> Self {
311        Self(node_id, kind)
312    }
313}
314impl NetHandler<Resource> for ImageHandler {
315    fn bytes(self: Box<Self>, doc_id: usize, bytes: Bytes, callback: SharedCallback<Resource>) {
316        // Try parse image
317        if let Ok(image) = image::ImageReader::new(Cursor::new(&bytes))
318            .with_guessed_format()
319            .expect("IO errors impossible with Cursor")
320            .decode()
321        {
322            let raw_rgba8_data = image.clone().into_rgba8().into_raw();
323            callback.call(
324                doc_id,
325                Ok(Resource::Image(
326                    self.0,
327                    self.1,
328                    image.width(),
329                    image.height(),
330                    Arc::new(raw_rgba8_data),
331                )),
332            );
333            return;
334        };
335
336        #[cfg(feature = "svg")]
337        {
338            use crate::util::parse_svg;
339            if let Ok(tree) = parse_svg(&bytes) {
340                callback.call(doc_id, Ok(Resource::Svg(self.0, self.1, Box::new(tree))));
341                return;
342            }
343        }
344
345        callback.call(doc_id, Err(Some(String::from("Could not parse image"))))
346    }
347}