Skip to main content

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}