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 {
175            self.0 = match bytes.as_ref() {
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"))]
179                // [b'w', b'O', b'F', b'F', ..] => 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'w', b'O', b'F', b'2', ..] => 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'O', b'T', b'T', b'O', ..] => 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't', b'r', b'u', b'e', ..] => 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(feature = "woff-c")]
203            // FontFaceSourceFormatKeyword::Woff => {
204            //     #[cfg(feature = "tracing")]
205            //     tracing::info!("Decompressing woff1 font");
206
207            //     // Use woff crate to decompress font
208            //     let decompressed = woff::version1::decompress(&bytes);
209
210            //     if let Some(decompressed) = decompressed {
211            //         bytes = Bytes::from(decompressed);
212            //     } else {
213            //         #[cfg(feature = "tracing")]
214            //         tracing::warn!("Failed to decompress woff1 font");
215            //     }
216            // }
217            #[cfg(any(feature = "woff-c", feature = "woff-rust"))]
218            FontFaceSourceFormatKeyword::Woff2 => {
219                #[cfg(feature = "tracing")]
220                tracing::info!("Decompressing woff2 font");
221
222                // Use woff crate to decompress font
223                #[cfg(feature = "woff-c")]
224                let decompressed = woff::version2::decompress(&bytes);
225
226                // Use woff2 crate to decompress font
227                #[cfg(feature = "woff-rust")]
228                let decompressed = woff2::decode::convert_woff2_to_ttf(&mut bytes).ok();
229
230                if let Some(decompressed) = decompressed {
231                    bytes = Bytes::from(decompressed);
232                } else {
233                    #[cfg(feature = "tracing")]
234                    tracing::warn!("Failed to decompress woff2 font");
235                }
236            }
237            FontFaceSourceFormatKeyword::None => {
238                return;
239            }
240            _ => {}
241        }
242
243        callback.call(doc_id, Ok(Resource::Font(bytes)))
244    }
245}
246
247fn fetch_font_face(
248    doc_id: usize,
249    sheet: &Stylesheet,
250    network_provider: &SharedProvider<Resource>,
251    read_guard: &SharedRwLockReadGuard,
252) {
253    sheet
254        .rules(read_guard)
255        .iter()
256        .filter_map(|rule| match rule {
257            CssRule::FontFace(font_face) => font_face.read_with(read_guard).sources.as_ref(),
258            _ => None,
259        })
260        .flat_map(|source_list| &source_list.0)
261        .filter_map(|source| match source {
262            Source::Url(url_source) => Some(url_source),
263            _ => None,
264        })
265        .for_each(|url_source| {
266            let mut format = match &url_source.format_hint {
267                Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
268                Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
269                    "woff2" => FontFaceSourceFormatKeyword::Woff2,
270                    "ttf" => FontFaceSourceFormatKeyword::Truetype,
271                    "otf" => FontFaceSourceFormatKeyword::Opentype,
272                    _ => FontFaceSourceFormatKeyword::None,
273                },
274                _ => FontFaceSourceFormatKeyword::None,
275            };
276            if format == FontFaceSourceFormatKeyword::None {
277                let Some((_, end)) = url_source.url.as_str().rsplit_once('.') else {
278                    return;
279                };
280                format = match end {
281                    "woff2" => FontFaceSourceFormatKeyword::Woff2,
282                    "woff" => FontFaceSourceFormatKeyword::Woff,
283                    "ttf" => FontFaceSourceFormatKeyword::Truetype,
284                    "otf" => FontFaceSourceFormatKeyword::Opentype,
285                    "svg" => FontFaceSourceFormatKeyword::Svg,
286                    "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
287                    _ => FontFaceSourceFormatKeyword::None,
288                }
289            }
290            if let _font_format @ (FontFaceSourceFormatKeyword::Svg
291            | FontFaceSourceFormatKeyword::EmbeddedOpentype
292            | FontFaceSourceFormatKeyword::Woff) = format
293            {
294                #[cfg(feature = "tracing")]
295                tracing::warn!("Skipping unsupported font of type {:?}", _font_format);
296                return;
297            }
298            let url = url_source.url.url().unwrap().as_ref().clone();
299            network_provider.fetch(doc_id, Request::get(url), Box::new(FontFaceHandler(format)))
300        });
301}
302
303pub struct ImageHandler(usize, ImageType);
304impl ImageHandler {
305    pub fn new(node_id: usize, kind: ImageType) -> Self {
306        Self(node_id, kind)
307    }
308}
309impl NetHandler<Resource> for ImageHandler {
310    fn bytes(self: Box<Self>, doc_id: usize, bytes: Bytes, callback: SharedCallback<Resource>) {
311        // Try parse image
312        if let Ok(image) = image::ImageReader::new(Cursor::new(&bytes))
313            .with_guessed_format()
314            .expect("IO errors impossible with Cursor")
315            .decode()
316        {
317            let raw_rgba8_data = image.clone().into_rgba8().into_raw();
318            callback.call(
319                doc_id,
320                Ok(Resource::Image(
321                    self.0,
322                    self.1,
323                    image.width(),
324                    image.height(),
325                    Arc::new(raw_rgba8_data),
326                )),
327            );
328            return;
329        };
330
331        #[cfg(feature = "svg")]
332        {
333            use crate::util::parse_svg;
334            if let Ok(tree) = parse_svg(&bytes) {
335                callback.call(doc_id, Ok(Resource::Svg(self.0, self.1, Box::new(tree))));
336                return;
337            }
338        }
339
340        callback.call(doc_id, Err(Some(String::from("Could not parse image"))))
341    }
342}