acdc_parser/source_provider.rs
1use std::{
2 fmt,
3 fs::File,
4 io::{Cursor, Read},
5 path::PathBuf,
6 sync::Arc,
7};
8
9/// A fully resolved include target presented to an [`IncludeSourceProvider`].
10///
11/// The parser applies attribute substitution, relative-path resolution, and
12/// Safe/Server confinement before constructing this value. Providers therefore
13/// supply bytes; they do not reinterpret the document's raw include target.
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15#[non_exhaustive]
16pub enum IncludeSourceTarget {
17 /// A local filesystem or virtual-filesystem path.
18 File(PathBuf),
19 /// An absolute HTTP(S) URI.
20 Uri(String),
21}
22
23/// An opened include source.
24///
25/// Providers can return an in-memory buffer or an owned byte reader.
26pub struct IncludeSource {
27 reader: Box<dyn Read>,
28 #[cfg(feature = "network")]
29 pub(crate) read_limit: Option<usize>,
30}
31
32impl IncludeSource {
33 /// Create an include source from an owned reader.
34 #[must_use]
35 pub fn from_reader(reader: impl Read + 'static) -> Self {
36 Self {
37 reader: Box::new(reader),
38 #[cfg(feature = "network")]
39 read_limit: None,
40 }
41 }
42
43 /// Create an include source from owned bytes.
44 #[must_use]
45 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
46 Self::from_reader(Cursor::new(bytes.into()))
47 }
48
49 /// Create an include source from an owned UTF-8 string.
50 #[must_use]
51 pub fn from_string(source: impl Into<String>) -> Self {
52 Self::from_bytes(source.into().into_bytes())
53 }
54
55 pub(crate) fn into_reader(self) -> Box<dyn Read> {
56 self.reader
57 }
58}
59
60impl fmt::Debug for IncludeSource {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter
63 .debug_struct("IncludeSource")
64 .finish_non_exhaustive()
65 }
66}
67
68/// Classification of a source-provider failure.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70#[non_exhaustive]
71pub enum IncludeSourceErrorKind {
72 /// The resolved source does not exist.
73 NotFound,
74 /// The source exists conceptually but could not be opened or retrieved.
75 Unavailable,
76 /// The provider does not support this target kind or transport.
77 Unsupported,
78 /// The provider cannot recover from this failure.
79 Fatal,
80}
81
82/// Failure returned by an [`IncludeSourceProvider`].
83#[derive(Debug, thiserror::Error)]
84#[error("{message}")]
85pub struct IncludeSourceError {
86 kind: IncludeSourceErrorKind,
87 message: String,
88}
89
90impl IncludeSourceError {
91 /// Create a classified source-provider error.
92 #[must_use]
93 pub fn new(kind: IncludeSourceErrorKind, message: impl Into<String>) -> Self {
94 Self {
95 kind,
96 message: message.into(),
97 }
98 }
99
100 /// Return this failure's classification.
101 #[must_use]
102 pub const fn kind(&self) -> IncludeSourceErrorKind {
103 self.kind
104 }
105}
106
107/// Supplies bytes for include targets resolved by the parser.
108///
109/// Implement this trait to parse against an editor overlay, virtual filesystem,
110/// embedded source map, restricted filesystem capability, or custom HTTP client.
111/// The parser checks safe mode and URI permission before calling the provider.
112pub trait IncludeSourceProvider {
113 /// Open one resolved include target.
114 ///
115 /// # Errors
116 ///
117 /// Return a classified [`IncludeSourceError`] when the target cannot be
118 /// supplied. The parser maps recoverable failures to Asciidoctor-compatible
119 /// include diagnostics and fallbacks.
120 fn open(&self, target: &IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError>;
121}
122
123impl<F> IncludeSourceProvider for F
124where
125 F: Fn(&IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError>,
126{
127 fn open(&self, target: &IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError> {
128 self(target)
129 }
130}
131
132/// Selects how the parser loads include sources.
133///
134/// [`crate::SafeMode`] sets processing restrictions independently of this choice.
135/// Safe-mode path checks and `allow-uri-read` apply to every provider.
136/// The default is [`Self::System`]. Secure mode never calls a provider.
137#[derive(Clone, Default)]
138#[non_exhaustive]
139pub enum IncludeLoader {
140 /// Do not load include targets. Preserve directives unless Secure mode
141 /// requires a link fallback.
142 ///
143 /// Applies to string, reader, and file input. Disabled includes are reported
144 /// through source recovery diagnostics.
145 Disabled,
146 /// Load local targets from the operating-system filesystem.
147 /// With `network`, also load permitted HTTP(S) targets outside bare WebAssembly.
148 ///
149 /// This is the default for all parse entry points.
150 #[default]
151 System,
152 /// Load sources through a caller-supplied provider.
153 Custom(Arc<dyn IncludeSourceProvider + Send + Sync>),
154}
155
156impl IncludeLoader {
157 /// Load include sources through `provider`.
158 #[must_use]
159 pub fn custom(provider: impl IncludeSourceProvider + Send + Sync + 'static) -> Self {
160 Self::Custom(Arc::new(provider))
161 }
162
163 /// Return the selected provider, or `None` when loading is disabled.
164 pub(crate) fn provider(&self) -> Option<&(dyn IncludeSourceProvider + '_)> {
165 static SYSTEM: SystemIncludeSourceProvider = SystemIncludeSourceProvider;
166 match self {
167 Self::Disabled => None,
168 Self::System => Some(&SYSTEM),
169 Self::Custom(provider) => Some(&**provider),
170 }
171 }
172}
173
174impl fmt::Debug for IncludeLoader {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 Self::Disabled => formatter.write_str("Disabled"),
178 Self::System => formatter.write_str("System"),
179 Self::Custom(_) => formatter.debug_tuple("Custom").finish_non_exhaustive(),
180 }
181 }
182}
183
184/// Operating-system provider used by [`IncludeLoader::System`].
185///
186/// Open local targets from the operating-system filesystem.
187/// Outside bare WebAssembly, `network` enables HTTP(S) loading through `ureq`.
188/// Otherwise, URI loading reports [`IncludeSourceErrorKind::Unsupported`].
189/// On targets such as `wasm32-unknown-unknown`, callers must fetch remote content
190/// before parsing and supply it through a custom [`IncludeSourceProvider`].
191///
192/// HTTP(S) reads have a separate 10 MiB limit after decompression.
193/// A finite line selection closes the response when complete.
194/// The parser does not read or check the rest of the response.
195#[derive(Clone, Copy, Debug, Default)]
196struct SystemIncludeSourceProvider;
197
198impl IncludeSourceProvider for SystemIncludeSourceProvider {
199 fn open(&self, target: &IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError> {
200 match target {
201 IncludeSourceTarget::File(path) => {
202 // Preserve the parser's established classification for
203 // directories, broken symlinks, and metadata failures: they are
204 // missing include files rather than opened-but-unreadable sources.
205 if !path.is_file() {
206 return Err(IncludeSourceError::new(
207 IncludeSourceErrorKind::NotFound,
208 format!("include file not found: {}", path.display()),
209 ));
210 }
211 File::open(path)
212 .map(IncludeSource::from_reader)
213 .map_err(|error| {
214 let kind = if error.kind() == std::io::ErrorKind::NotFound {
215 IncludeSourceErrorKind::NotFound
216 } else {
217 IncludeSourceErrorKind::Unavailable
218 };
219 IncludeSourceError::new(kind, error.to_string())
220 })
221 }
222 IncludeSourceTarget::Uri(uri) => {
223 #[cfg(all(
224 feature = "network",
225 not(all(target_family = "wasm", target_os = "unknown"))
226 ))]
227 {
228 let response = ureq::get(uri).call().map_err(|error| {
229 IncludeSourceError::new(
230 IncludeSourceErrorKind::Unavailable,
231 error.to_string(),
232 )
233 })?;
234 let (_, body) = response.into_parts();
235 let mut source = IncludeSource::from_reader(body.into_reader());
236 source.read_limit = Some(10 * 1024 * 1024);
237 Ok(source)
238 }
239 #[cfg(any(
240 not(feature = "network"),
241 all(target_family = "wasm", target_os = "unknown")
242 ))]
243 {
244 let _ = uri;
245 let message = if cfg!(all(target_family = "wasm", target_os = "unknown")) {
246 "built-in HTTP(S) includes are unavailable on this WebAssembly target. Fetch content first and supply it through a custom include provider"
247 } else {
248 "network support is disabled"
249 };
250 Err(IncludeSourceError::new(
251 IncludeSourceErrorKind::Unsupported,
252 message,
253 ))
254 }
255 }
256 }
257 }
258}