1use apace_traits::node_id::NodeId;
2use selectors::context::QuirksMode;
3use std::{io::Cursor, sync::Arc};
4use style::{
5 font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, FontStyleRange, Source},
6 media_queries::MediaList,
7 servo_arc::Arc as ServoArc,
8 shared_lock::SharedRwLock,
9 shared_lock::{Locked, SharedRwLockReadGuard},
10 stylesheets::{
11 AllowImportRules, CssRule, DocumentStyleSheet, ImportRule, Origin, Stylesheet,
12 StylesheetInDocument, StylesheetLoader as ServoStylesheetLoader, UrlExtraData,
13 import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition},
14 },
15 values::{CssUrl, SourceLocation},
16};
17
18use crate::resource_queue::{DocumentSender, Ticket};
19use apace_traits::net::{AbortSignal, Bytes, NetHandler, NetProvider, NetReady, Request};
20use apace_traits::shell::ShellProvider;
21use std::task::Poll;
22
23use url::Url;
24
25use crate::{document::DocumentEvent, util::ImageType};
26
27pub(crate) fn stamped_request(url: Url, signal: Option<&AbortSignal>) -> Request {
28 let mut req = Request::get(url);
29 if let Some(sig) = signal {
30 req = req.signal(sig.clone());
31 }
32 req
33}
34
35#[derive(Clone, Debug, Default)]
44pub struct FontFaceOverrides {
45 pub family_name: Option<String>,
47 pub weight: Option<f32>,
51 pub style: Option<parley::fontique::FontStyle>,
53}
54
55#[derive(Clone, Debug)]
56pub enum Resource {
57 Image(ImageType, u32, u32, Arc<Vec<u8>>),
58 #[cfg(feature = "svg")]
59 Svg(ImageType, crate::node::SvgImageData),
60 Css(DocumentStyleSheet),
61 ImportedCss(ServoArc<Locked<ImportRule>>, ServoArc<Stylesheet>),
63 Font(Bytes, FontFaceOverrides),
64 DocumentSrc(String),
66 None,
67}
68
69pub(crate) trait Payload: Send + Sync + 'static {
70 fn weight(&self, bytes: &Bytes) -> Result<usize, String> {
71 Ok(bytes.len())
72 }
73}
74pub(crate) struct ResourceHandler<T: Payload> {
75 ticket: Ticket,
76 doc_id: usize,
77 request_id: usize,
78 node_id: Option<NodeId>,
79 tx: DocumentSender,
80 shell_provider: Arc<dyn ShellProvider>,
81 data: T,
82}
83
84impl<T: Payload> ResourceHandler<T> {
85 pub(crate) fn new(
86 tx: DocumentSender,
87 doc_id: usize,
88 node_id: Option<NodeId>,
89 shell_provider: Arc<dyn ShellProvider>,
90 data: T,
91 ) -> Option<Self> {
92 let ticket = tx.pool.register()?;
93 Some(Self::with_ticket(
94 tx,
95 doc_id,
96 node_id,
97 shell_provider,
98 data,
99 ticket,
100 ))
101 }
102 fn with_ticket(
103 tx: DocumentSender,
104 doc_id: usize,
105 node_id: Option<NodeId>,
106 shell_provider: Arc<dyn ShellProvider>,
107 data: T,
108 ticket: Ticket,
109 ) -> Self {
110 Self {
111 request_id: ticket.id(),
112 ticket,
113 doc_id,
114 node_id,
115 tx,
116 shell_provider,
117 data,
118 }
119 }
120
121 pub(crate) fn boxed(
122 tx: DocumentSender,
123 doc_id: usize,
124 node_id: Option<NodeId>,
125 shell_provider: Arc<dyn ShellProvider>,
126 data: T,
127 ) -> Option<Box<dyn NetHandler>>
128 where
129 ResourceHandler<T>: NetHandler,
130 {
131 Some(Box::new(Self::new(tx, doc_id, node_id, shell_provider, data)?) as _)
132 }
133
134 pub(crate) fn request_id(&self) -> usize {
135 self.request_id
136 }
137
138 fn cost(&self, bytes: &Bytes) -> Result<usize, String> {
139 if !self.ticket.live() {
140 return Err("retired resource".into());
141 }
142 if bytes.len() > self.ticket.limits().encoded_bytes {
143 self.ticket.reject();
144 return Err("encoded resource budget".into());
145 }
146 self.data.weight(bytes)
147 }
148 fn prepare_bytes(&self, bytes: &Bytes) -> NetReady<'static> {
149 match self.cost(bytes) {
150 Ok(bytes) => Box::pin(self.ticket.wait(bytes)),
151 Err(error) => Box::pin(std::future::ready(Err(error))),
152 }
153 }
154 fn admit(&self, bytes: &Bytes) -> Result<(), String> {
155 let bytes = self.cost(bytes)?;
156 match self.ticket.reserve(bytes, None) {
157 Poll::Ready(result) => result,
158 Poll::Pending => {
159 self.ticket.reject();
160 Err("resource completion capacity; await prepare".into())
161 }
162 }
163 }
164 fn respond(&self, mut resolved_url: String, mut result: Result<Resource, String>) {
165 if resolved_url.len() > 4096 {
166 cut(&mut resolved_url, 4096);
167 result = Err("resource response URL budget".into());
168 }
169 if let Err(error) = &mut result {
170 cut(error, 512);
171 }
172 let weight = match &result {
175 Ok(Resource::Image(_, _, _, bytes)) => Some(bytes.len()),
176 Ok(Resource::Font(bytes, overrides)) => Some(
177 bytes
178 .len()
179 .saturating_add(overrides.family_name.as_ref().map_or(0, String::len)),
180 ),
181 Ok(Resource::DocumentSrc(html)) => Some(html.len()),
182 Ok(Resource::None) | Err(_) => Some(0),
183 _ => None,
184 };
185 if let Some(weight) = weight {
186 if !matches!(self.ticket.reserve(weight, None), Poll::Ready(Ok(()))) {
187 result = Err("resource completion byte budget".into());
188 let _ = self.ticket.reserve(0, None);
189 }
190 }
191 let Some(notify) = self.ticket.publish() else {
192 return;
193 };
194 let response = ResourceLoadResponse {
195 ticket: Some(self.ticket.clone()),
196 request_id: self.request_id,
197 node_id: self.node_id,
198 resolved_url: Some(resolved_url),
199 result,
200 };
201 let _ = self.tx.send(DocumentEvent::ResourceLoad(response));
202 if notify {
203 self.shell_provider.request_redraw();
204 }
205 }
206}
207
208fn cut(text: &mut String, limit: usize) {
209 let mut end = text.len().min(limit);
210 while !text.is_char_boundary(end) {
211 end -= 1;
212 }
213 if text.len() > end || text.capacity() > limit {
216 *text = text[..end].to_owned();
217 }
218}
219
220#[allow(unused)]
221pub struct ResourceLoadResponse {
222 pub request_id: usize,
223 pub node_id: Option<NodeId>,
224 pub resolved_url: Option<String>,
225 pub result: Result<Resource, String>,
226 pub(crate) ticket: Option<Ticket>,
229}
230
231pub struct StylesheetHandler {
232 pub guard: SharedRwLock,
233 pub net_provider: Arc<dyn NetProvider>,
234 pub abort_signal: Option<AbortSignal>,
235}
236impl Payload for StylesheetHandler {}
237
238impl NetHandler for ResourceHandler<StylesheetHandler> {
239 fn prepare<'a>(&'a mut self, _url: &'a str, bytes: &'a Bytes) -> NetReady<'a> {
240 self.prepare_bytes(bytes)
241 }
242 fn error(self: Box<Self>, resolved_url: String, error: String) {
243 self.respond(resolved_url, Err(error));
244 }
245
246 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
247 if let Err(error) = self.admit(&bytes) {
248 return self.respond(resolved_url, Err(error));
249 }
250 let Ok(css) = std::str::from_utf8(&bytes) else {
251 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
252 };
253
254 let Ok(source_url) = Url::parse(&resolved_url) else {
255 return self.respond(resolved_url, Err(String::from("Invalid stylesheet URL")));
256 };
257
258 let sheet = Stylesheet::from_str(
262 css,
263 source_url.into(),
264 Origin::Author,
265 ServoArc::new(self.data.guard.wrap(MediaList::empty())),
266 self.data.guard.clone(),
267 Some(&StylesheetLoader {
268 tx: self.tx.clone(),
269 doc_id: self.doc_id,
270 net_provider: self.data.net_provider.clone(),
271 shell_provider: self.shell_provider.clone(),
272 abort_signal: self.data.abort_signal.clone(),
273 import_depth: 0,
274 }),
275 None, QuirksMode::NoQuirks,
277 AllowImportRules::Yes,
278 );
279
280 self.respond(
281 resolved_url,
282 Ok(Resource::Css(DocumentStyleSheet(ServoArc::new(sheet)))),
283 );
284 }
285}
286
287const MAX_IMPORT_DEPTH: usize = 16;
292
293#[derive(Clone)]
294pub(crate) struct StylesheetLoader {
295 pub(crate) tx: DocumentSender,
296 pub(crate) doc_id: usize,
297 pub(crate) net_provider: Arc<dyn NetProvider>,
298 pub(crate) shell_provider: Arc<dyn ShellProvider>,
299 pub(crate) abort_signal: Option<AbortSignal>,
300 pub(crate) import_depth: usize,
303}
304impl ServoStylesheetLoader for StylesheetLoader {
305 fn request_stylesheet(
306 &self,
307 url: CssUrl,
308 location: SourceLocation,
309 lock: &SharedRwLock,
310 media: ServoArc<Locked<MediaList>>,
311 supports: Option<ImportSupportsCondition>,
312 layer: ImportLayer,
313 ) -> ServoArc<Locked<ImportRule>> {
314 let ticket = self.tx.pool.register();
315 if self.import_depth >= MAX_IMPORT_DEPTH
316 || !supports.as_ref().is_none_or(|s| s.enabled)
317 || ticket.is_none()
318 {
319 return ServoArc::new(lock.wrap(ImportRule {
320 url,
321 stylesheet: ImportSheet::new_refused(),
322 supports,
323 layer,
324 source_location: location,
325 }));
326 }
327
328 let import = ImportRule {
329 url,
330 stylesheet: ImportSheet::new_pending(),
331 supports,
332 layer,
333 source_location: location,
334 };
335
336 let url = import.url.url().unwrap().clone();
337 let import = ServoArc::new(lock.wrap(import));
338 self.net_provider.fetch(
339 self.doc_id,
340 stamped_request(url.as_ref().clone(), self.abort_signal.as_ref()),
341 Box::new(ResourceHandler::with_ticket(
342 self.tx.clone(),
343 self.doc_id,
344 None, self.shell_provider.clone(),
346 NestedStylesheetHandler {
347 loader: StylesheetLoader {
348 import_depth: self.import_depth + 1,
349 ..self.clone()
350 },
351 lock: lock.clone(),
352 media,
353 import_rule: import.clone(),
354 net_provider: self.net_provider.clone(),
355 },
356 ticket.expect("admitted stylesheet"),
357 )),
358 );
359
360 import
361 }
362}
363
364struct NestedStylesheetHandler {
365 loader: StylesheetLoader,
366 lock: SharedRwLock,
367 media: ServoArc<Locked<MediaList>>,
368 import_rule: ServoArc<Locked<ImportRule>>,
369 net_provider: Arc<dyn NetProvider>,
370}
371
372impl Payload for NestedStylesheetHandler {}
373impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
374 fn prepare<'a>(&'a mut self, _url: &'a str, bytes: &'a Bytes) -> NetReady<'a> {
375 self.prepare_bytes(bytes)
376 }
377 fn error(self: Box<Self>, resolved_url: String, error: String) {
378 self.respond(resolved_url, Err(error));
379 }
380
381 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
382 if let Err(error) = self.admit(&bytes) {
383 return self.respond(resolved_url, Err(error));
384 }
385 let Ok(css) = std::str::from_utf8(&bytes) else {
386 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
387 };
388
389 let Ok(source_url) = Url::parse(&resolved_url) else {
390 return self.respond(resolved_url, Err(String::from("Invalid stylesheet URL")));
391 };
392
393 let sheet = ServoArc::new(Stylesheet::from_str(
397 css,
398 UrlExtraData(ServoArc::new(source_url)),
399 Origin::Author,
400 self.data.media.clone(),
401 self.data.lock.clone(),
402 Some(&self.data.loader),
403 None, QuirksMode::NoQuirks,
405 AllowImportRules::Yes,
406 ));
407
408 fetch_font_face(
410 self.tx.clone(),
411 self.doc_id,
412 self.node_id,
413 &sheet,
414 &self.data.net_provider,
415 &self.shell_provider,
416 &self.data.lock.read(),
417 self.data.loader.abort_signal.as_ref(),
418 );
419
420 let import_rule = self.data.import_rule.clone();
421 self.respond(resolved_url, Ok(Resource::ImportedCss(import_rule, sheet)))
422 }
423}
424
425struct FontFaceHandler {
426 format: FontFaceSourceFormatKeyword,
427 overrides: FontFaceOverrides,
428}
429impl Payload for FontFaceHandler {
430 fn weight(&self, bytes: &Bytes) -> Result<usize, String> {
431 bytes
432 .len()
433 .checked_add(self.overrides.family_name.as_ref().map_or(0, String::len))
434 .ok_or_else(|| "font size overflow".into())
435 }
436}
437impl NetHandler for ResourceHandler<FontFaceHandler> {
438 fn prepare<'a>(&'a mut self, _url: &'a str, bytes: &'a Bytes) -> NetReady<'a> {
439 self.prepare_bytes(bytes)
440 }
441 fn error(self: Box<Self>, resolved_url: String, error: String) {
442 self.respond(resolved_url, Err(error));
443 }
444
445 fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
446 if let Err(error) = self.admit(&bytes) {
447 return self.respond(resolved_url, Err(error));
448 }
449 let result = self.data.parse(bytes);
450 self.respond(resolved_url, result)
451 }
452}
453impl FontFaceHandler {
454 fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
455 if self.format == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
456 self.format = match &bytes.as_ref()[0..4] {
457 #[cfg(feature = "woff")]
460 b"wOFF" => FontFaceSourceFormatKeyword::Woff,
461 #[cfg(feature = "woff")]
464 b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
465 b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
468 &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
471 b"true" => FontFaceSourceFormatKeyword::Truetype,
474 _ => FontFaceSourceFormatKeyword::None,
475 }
476 }
477
478 #[cfg(feature = "woff")]
480 let mut bytes = bytes;
481
482 match self.format {
483 #[cfg(feature = "woff")]
484 FontFaceSourceFormatKeyword::Woff => {
485 #[cfg(feature = "tracing")]
486 tracing::info!("Decompressing woff1 font");
487
488 let decompressed = wuff::decompress_woff1(&bytes).ok();
490
491 if let Some(decompressed) = decompressed {
492 bytes = Bytes::from(decompressed);
493 } else {
494 #[cfg(feature = "tracing")]
495 tracing::warn!("Failed to decompress woff1 font");
496 }
497 }
498 #[cfg(feature = "woff")]
499 FontFaceSourceFormatKeyword::Woff2 => {
500 #[cfg(feature = "tracing")]
501 tracing::info!("Decompressing woff2 font");
502
503 let decompressed = wuff::decompress_woff2(&bytes).ok();
505
506 if let Some(decompressed) = decompressed {
507 bytes = Bytes::from(decompressed);
508 } else {
509 #[cfg(feature = "tracing")]
510 tracing::warn!("Failed to decompress woff2 font");
511 }
512 }
513 FontFaceSourceFormatKeyword::None => {
514 return Ok(Resource::None);
516 }
517 _ => {}
518 }
519
520 Ok(Resource::Font(bytes, std::mem::take(&mut self.overrides)))
521 }
522}
523
524#[allow(clippy::too_many_arguments)]
526pub(crate) fn fetch_font_face(
527 tx: DocumentSender,
528 doc_id: usize,
529 node_id: Option<NodeId>,
530 sheet: &Stylesheet,
531 network_provider: &Arc<dyn NetProvider>,
532 shell_provider: &Arc<dyn ShellProvider>,
533 read_guard: &SharedRwLockReadGuard,
534 abort_signal: Option<&AbortSignal>,
535) {
536 fetch_font_face_rules(
537 sheet.contents(read_guard).rules(read_guard).iter(),
538 tx,
539 doc_id,
540 node_id,
541 network_provider,
542 shell_provider,
543 read_guard,
544 abort_signal,
545 )
546}
547
548#[allow(clippy::too_many_arguments)]
550pub(crate) fn fetch_font_face_rules<'a>(
551 rules: impl Iterator<Item = &'a CssRule>,
552 tx: DocumentSender,
553 doc_id: usize,
554 node_id: Option<NodeId>,
555 network_provider: &Arc<dyn NetProvider>,
556 shell_provider: &Arc<dyn ShellProvider>,
557 read_guard: &SharedRwLockReadGuard,
558 abort_signal: Option<&AbortSignal>,
559) {
560 rules
561 .filter_map(|rule| match rule {
562 CssRule::FontFace(font_face) => {
563 let descriptor = &font_face.read_with(read_guard).descriptors;
564 let family = descriptor.font_family.as_ref()?;
565 let src = descriptor.src.as_ref()?;
566 let overrides = FontFaceOverrides {
570 family_name: Some(family.name.to_string()),
571 weight: descriptor
572 .font_weight
573 .as_ref()
574 .and_then(|range| range.0.compute().map(|w| w.value())),
575 style: descriptor.font_style.as_ref().map(stylo_to_fontique_style),
576 };
577 Some((src, overrides))
578 }
579 _ => None,
580 })
581 .for_each(|(source_list, overrides)| {
582 let preferred_source = source_list
585 .0
586 .iter()
587 .filter_map(|source| match source {
588 Source::Url(url_source) => Some(url_source),
589 Source::Local(_) => None,
591 })
592 .find_map(|url_source| {
593 let mut format = match &url_source.format_hint {
594 Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
595 Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
596 "woff2" => FontFaceSourceFormatKeyword::Woff2,
597 "ttf" => FontFaceSourceFormatKeyword::Truetype,
598 "otf" => FontFaceSourceFormatKeyword::Opentype,
599 _ => FontFaceSourceFormatKeyword::None,
600 },
601 _ => FontFaceSourceFormatKeyword::None,
602 };
603 if format == FontFaceSourceFormatKeyword::None
606 && let Some((_, end)) = url_source.url.as_str().rsplit_once('.')
607 {
608 format = match end {
609 "woff2" => FontFaceSourceFormatKeyword::Woff2,
610 "woff" => FontFaceSourceFormatKeyword::Woff,
611 "ttf" => FontFaceSourceFormatKeyword::Truetype,
612 "otf" => FontFaceSourceFormatKeyword::Opentype,
613 "svg" => FontFaceSourceFormatKeyword::Svg,
614 "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
615 _ => FontFaceSourceFormatKeyword::None,
616 }
617 }
618
619 if matches!(
620 format,
621 FontFaceSourceFormatKeyword::Svg
622 | FontFaceSourceFormatKeyword::EmbeddedOpentype
623 ) {
624 #[cfg(feature = "tracing")]
625 tracing::warn!("Skipping unsupported font of type {:?}", format);
626 return None;
627 }
628
629 #[cfg(not(feature = "woff"))]
630 if matches!(
631 format,
632 FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
633 ) {
634 #[cfg(feature = "tracing")]
635 tracing::warn!("Skipping unsupported font of type {:?}", format);
636 return None;
637 }
638
639 let Some(url) = url_source.url.url() else {
642 #[cfg(feature = "tracing")]
643 tracing::warn!("Skipping @font-face source with unresolvable url");
644 return None;
645 };
646 let url = url.as_ref().clone();
647 Some((url, format))
648 });
649
650 if let Some((url, format)) = preferred_source {
651 let Some(handler) = ResourceHandler::boxed(
652 tx.clone(),
653 doc_id,
654 node_id,
655 shell_provider.clone(),
656 FontFaceHandler { format, overrides },
657 ) else {
658 return;
659 };
660 network_provider.fetch(doc_id, stamped_request(url, abort_signal), handler);
661 }
662 })
663}
664
665fn stylo_to_fontique_style(style: &FontStyleRange) -> parley::fontique::FontStyle {
671 use parley::fontique::FontStyle as Fq;
672 match style {
673 FontStyleRange::Italic => Fq::Italic,
674 FontStyleRange::Oblique(min, max) => {
675 let angle = min.degrees();
676 if angle.is_none_or(|a| a == 0.0) && max.degrees().is_none_or(|a| a == 0.0) {
680 Fq::Normal
681 } else {
682 Fq::Oblique(angle)
683 }
684 }
685 }
686}
687
688pub(crate) struct DocumentSrcHandler;
690
691impl Payload for DocumentSrcHandler {}
692impl NetHandler for ResourceHandler<DocumentSrcHandler> {
693 fn prepare<'a>(&'a mut self, _url: &'a str, bytes: &'a Bytes) -> NetReady<'a> {
694 self.prepare_bytes(bytes)
695 }
696 fn error(self: Box<Self>, resolved_url: String, error: String) {
697 self.respond(resolved_url, Err(error));
698 }
699
700 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
701 if let Err(error) = self.admit(&bytes) {
702 return self.respond(resolved_url, Err(error));
703 }
704 let html = String::from_utf8_lossy(&bytes).into_owned();
705 self.respond(resolved_url, Ok(Resource::DocumentSrc(html)));
706 }
707}
708
709pub struct ImageHandler {
710 kind: ImageType,
711 limits: crate::ResourceLimits,
712}
713impl ImageHandler {
714 pub fn with_limits(kind: ImageType, limits: crate::ResourceLimits) -> Self {
715 Self { kind, limits }
716 }
717 pub fn new(kind: ImageType) -> Self {
718 Self {
719 kind,
720 limits: crate::ResourceLimits::default(),
721 }
722 }
723}
724
725impl Payload for ImageHandler {
726 fn weight(&self, bytes: &Bytes) -> Result<usize, String> {
727 match image::ImageReader::new(Cursor::new(bytes))
728 .with_guessed_format()
729 .map_err(|e| e.to_string())?
730 .into_dimensions()
731 {
732 Ok((w, h)) => {
733 let bytes = u128::from(w) * u128::from(h) * 4;
734 if w > self.limits.image_dimension
735 || h > self.limits.image_dimension
736 || bytes > u128::from(self.limits.image_decoded_bytes)
737 {
738 return Err("decoded image exceeds resource limits".into());
739 }
740 usize::try_from(bytes).map_err(|_| "image size overflow".into())
741 }
742 Err(error) => {
743 #[cfg(feature = "svg")]
744 {
745 let _ = error;
746 Ok(bytes.len())
747 }
748 #[cfg(not(feature = "svg"))]
749 {
750 Err(error.to_string())
751 }
752 }
753 }
754 }
755}
756impl NetHandler for ResourceHandler<ImageHandler> {
757 fn prepare<'a>(&'a mut self, _url: &'a str, bytes: &'a Bytes) -> NetReady<'a> {
758 self.prepare_bytes(bytes)
759 }
760 fn error(self: Box<Self>, resolved_url: String, error: String) {
761 self.respond(resolved_url, Err(error));
762 }
763
764 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
765 if let Err(error) = self.admit(&bytes) {
766 return self.respond(resolved_url, Err(error));
767 }
768 let result = self.data.parse(bytes);
769 self.respond(resolved_url, result)
770 }
771}
772
773impl ImageHandler {
774 fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
775 let dimensions = image::ImageReader::new(Cursor::new(&bytes))
778 .with_guessed_format()
779 .map_err(|error| error.to_string())?
780 .into_dimensions();
781 if let Ok((width, height)) = dimensions {
782 if width > self.limits.image_dimension
783 || height > self.limits.image_dimension
784 || u128::from(width) * u128::from(height) * 4
785 > u128::from(self.limits.image_decoded_bytes)
786 {
787 return Err("decoded image exceeds resource limits".into());
788 }
789 }
790 let image_err = match image::ImageReader::new(Cursor::new(&bytes))
791 .with_guessed_format()
792 .expect("IO errors impossible with Cursor")
793 .decode()
794 {
795 Ok(image) => {
796 let width = image.width();
797 let height = image.height();
798 let raw_rgba8_data = image.into_rgba8().into_raw();
799 return Ok(Resource::Image(
800 self.kind,
801 width,
802 height,
803 Arc::new(raw_rgba8_data),
804 ));
805 }
806 Err(e) => e.to_string(),
807 };
808
809 #[cfg(feature = "svg")]
810 let svg_err = {
811 use crate::util::parse_svg_image;
812 match parse_svg_image(&bytes) {
813 Ok(svg) => return Ok(Resource::Svg(self.kind, svg)),
814 Err(e) => e.to_string(),
815 }
816 };
817 #[cfg(not(feature = "svg"))]
818 let svg_err = "svg feature disabled";
819
820 Err(format!(
821 "Could not parse image ({} bytes): image-crate error: {image_err}; svg fallback error: {svg_err}",
822 bytes.len()
823 ))
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830 use parley::fontique::FontStyle as Fq;
831 use style::values::specified::Angle;
832
833 #[test]
834 fn response_diagnostics_are_bounded_utf8_and_do_not_retain_large_capacities() {
835 let doc = crate::BaseDocument::new(Default::default());
836 let handler = ResourceHandler::new(
837 doc.tx.clone(),
838 doc.id(),
839 None,
840 doc.shell_provider.clone(),
841 ImageHandler::new(ImageType::Image),
842 )
843 .unwrap();
844 Box::new(handler).error("🙂".repeat(10000), "🙂".repeat(10000));
845 let DocumentEvent::ResourceLoad(response) = doc.rx.as_ref().unwrap().try_recv().unwrap()
846 else {
847 panic!("resource")
848 };
849 let url = response.resolved_url.as_ref().unwrap();
850 assert!(url.len() <= 4096 && url.capacity() <= 4096);
851 let error = response.result.as_ref().unwrap_err();
852 assert!(error.len() <= 512 && error.capacity() <= 512);
853 assert_eq!(doc.resource_stats().requests, 1);
854 drop(response);
855 assert_eq!(doc.resource_stats().requests, 0);
856 }
857
858 fn oblique(min_deg: f32, max_deg: f32) -> FontStyleRange {
859 FontStyleRange::Oblique(Angle::from_degrees(min_deg), Angle::from_degrees(max_deg))
860 }
861
862 #[test]
863 fn italic_maps_to_italic() {
864 assert_eq!(stylo_to_fontique_style(&FontStyleRange::Italic), Fq::Italic,);
865 }
866
867 #[test]
868 fn oblique_zero_zero_maps_to_normal() {
869 assert_eq!(stylo_to_fontique_style(&oblique(0.0, 0.0)), Fq::Normal);
873 }
874
875 #[test]
876 fn oblique_single_angle_maps_to_oblique_with_min() {
877 assert_eq!(
878 stylo_to_fontique_style(&oblique(14.0, 14.0)),
879 Fq::Oblique(Some(14.0)),
880 );
881 }
882
883 #[test]
884 fn oblique_range_uses_min_angle() {
885 assert_eq!(
888 stylo_to_fontique_style(&oblique(10.0, 20.0)),
889 Fq::Oblique(Some(10.0)),
890 );
891 }
892}