1use crate::{InlineError, Result};
2use std::{borrow::Cow, io::ErrorKind, path::Path};
3
4fn 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#[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 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 fn retrieve_from_url(&self, url: &str) -> Result<String> {
71 Err(self.unsupported(&format!("Loading external URLs is not supported: {url}")))
72 }
73 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 #[cfg(feature = "async")]
98 async fn retrieve_async(&self, location: &str) -> Result<String> {
99 self.retrieve(location)
100 }
101 fn unsupported(&self, reason: &str) -> InlineError {
103 std::io::Error::new(ErrorKind::Unsupported, reason).into()
104 }
105}
106
107#[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#[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}