Skip to main content

css_inline/
resolver.rs

1use crate::{InlineError, Result};
2use std::{borrow::Cow, io::ErrorKind, path::Path};
3
4/// Turn a `file://` URL into a filesystem path.
5///
6/// Percent-decodes the path and handles Windows drive letters. URLs with a host
7/// (e.g. `file://relative-dir/`) keep the legacy prefix-stripping behavior.
8fn to_file_path(location: &str) -> Cow<'_, Path> {
9    #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
10    if let Ok(url) = url::Url::parse(location) {
11        if url.scheme() == "file" && url.host().is_none() {
12            if let Ok(path) = url.to_file_path() {
13                return Cow::Owned(path);
14            }
15        }
16    }
17    Cow::Borrowed(Path::new(location.trim_start_matches("file://")))
18}
19
20/// Way of resolving stylesheets from various sources.
21#[cfg_attr(
22    all(feature = "async", target_family = "wasm"),
23    async_trait::async_trait(?Send)
24)]
25#[cfg_attr(
26    all(feature = "async", not(target_family = "wasm")),
27    async_trait::async_trait
28)]
29pub trait StylesheetResolver: Send + Sync {
30    /// Retrieve a stylesheet from a network or local filesystem location.
31    ///
32    /// # Errors
33    ///
34    /// Any network or filesystem related error, or an error during response parsing.
35    fn retrieve(&self, location: &str) -> Result<String> {
36        if location.starts_with("https") || location.starts_with("http") {
37            #[cfg(feature = "http")]
38            {
39                self.retrieve_from_url(location)
40            }
41
42            #[cfg(not(feature = "http"))]
43            {
44                Err(std::io::Error::new(
45                    ErrorKind::Unsupported,
46                    "Loading external URLs requires the `http` feature",
47                )
48                .into())
49            }
50        } else {
51            #[cfg(feature = "file")]
52            {
53                self.retrieve_from_path(location)
54            }
55            #[cfg(not(feature = "file"))]
56            {
57                Err(std::io::Error::new(
58                    ErrorKind::Unsupported,
59                    "Loading local files requires the `file` feature",
60                )
61                .into())
62            }
63        }
64    }
65    /// Retrieve a stylesheet from a network location.
66    ///
67    /// # Errors
68    ///
69    /// Any network-related error, or an error during response parsing.
70    fn retrieve_from_url(&self, url: &str) -> Result<String> {
71        Err(self.unsupported(&format!("Loading external URLs is not supported: {url}")))
72    }
73    /// Retrieve a stylesheet from the local filesystem.
74    ///
75    /// # Errors
76    ///
77    /// Any filesystem-related error.
78    fn retrieve_from_path(&self, path: &str) -> Result<String> {
79        let path = to_file_path(path);
80        std::fs::read_to_string(&path).map_err(|error| match error.kind() {
81            ErrorKind::NotFound => InlineError::MissingStyleSheet {
82                path: path.display().to_string(),
83            },
84            #[cfg(target_family = "wasm")]
85            ErrorKind::Unsupported => self.unsupported(&format!(
86                "Loading local files is not supported on WASM: {}",
87                path.display()
88            )),
89            _ => InlineError::IO(error),
90        })
91    }
92    /// Non-blocking version of [`StylesheetResolver::retrieve`]. Calls it by default.
93    ///
94    /// # Errors
95    ///
96    /// Any network or filesystem related error, or an error during response parsing.
97    #[cfg(feature = "async")]
98    async fn retrieve_async(&self, location: &str) -> Result<String> {
99        self.retrieve(location)
100    }
101    /// Return the "Unsupported" kind of error.
102    fn unsupported(&self, reason: &str) -> InlineError {
103        std::io::Error::new(ErrorKind::Unsupported, reason).into()
104    }
105}
106
107/// TLS with `ring` and the bundled Mozilla roots, passed per client to leave the process-wide
108/// `rustls` provider to the host application.
109#[cfg(all(feature = "http", not(target_family = "wasm")))]
110fn tls_config() -> rustls::ClientConfig {
111    static CONFIG: std::sync::OnceLock<rustls::ClientConfig> = std::sync::OnceLock::new();
112    CONFIG
113        .get_or_init(|| {
114            rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
115                rustls::crypto::ring::default_provider(),
116            ))
117            .with_safe_default_protocol_versions()
118            .expect("`ring` supports the default protocol versions")
119            .with_root_certificates(rustls::RootCertStore {
120                roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
121            })
122            .with_no_client_auth()
123        })
124        .clone()
125}
126
127#[cfg(all(feature = "http", feature = "async", not(target_family = "wasm")))]
128fn async_client() -> reqwest::Result<reqwest::Client> {
129    reqwest::Client::builder()
130        .tls_backend_preconfigured(tls_config())
131        .build()
132}
133
134#[cfg(all(feature = "http", feature = "async", target_family = "wasm"))]
135fn async_client() -> reqwest::Result<reqwest::Client> {
136    reqwest::Client::builder().build()
137}
138
139/// Default stylesheet resolver.
140#[derive(Debug, Default)]
141pub struct DefaultStylesheetResolver;
142
143#[cfg_attr(
144    all(feature = "async", target_family = "wasm"),
145    async_trait::async_trait(?Send)
146)]
147#[cfg_attr(
148    all(feature = "async", not(target_family = "wasm")),
149    async_trait::async_trait
150)]
151impl StylesheetResolver for DefaultStylesheetResolver {
152    #[cfg(all(feature = "http", not(target_family = "wasm")))]
153    fn retrieve_from_url(&self, url: &str) -> Result<String> {
154        let into_error = |error| InlineError::Network {
155            error,
156            location: url.to_string(),
157        };
158        reqwest::blocking::Client::builder()
159            .tls_backend_preconfigured(tls_config())
160            .build()
161            .map_err(into_error)?
162            .get(url)
163            .send()
164            .map_err(into_error)?
165            .text()
166            .map_err(into_error)
167    }
168
169    #[cfg(feature = "async")]
170    async fn retrieve_async(&self, location: &str) -> Result<String> {
171        if !(location.starts_with("https") || location.starts_with("http")) {
172            #[cfg(target_family = "wasm")]
173            return self.retrieve(location);
174            #[cfg(not(target_family = "wasm"))]
175            {
176                let (tx, rx) = futures_channel::oneshot::channel();
177                let location = location.to_string();
178                std::thread::spawn(move || tx.send(DefaultStylesheetResolver.retrieve(&location)));
179                return rx.await.expect("Stylesheet reading thread panicked");
180            }
181        }
182        #[cfg(feature = "http")]
183        {
184            let into_error = |error| InlineError::Network {
185                error,
186                location: location.to_string(),
187            };
188            async_client()
189                .map_err(into_error)?
190                .get(location)
191                .send()
192                .await
193                .map_err(into_error)?
194                .text()
195                .await
196                .map_err(into_error)
197        }
198        #[cfg(not(feature = "http"))]
199        {
200            Err(self.unsupported("Loading external URLs requires the `http` feature"))
201        }
202    }
203}