1use std::{
4 ffi::CString,
5 fmt,
6 fs::File,
7 io,
8 path::{Path, PathBuf},
9};
10
11#[cfg(unix)]
12use std::os::unix::ffi::OsStrExt;
13
14#[cfg(windows)]
15use std::io::Read;
16
17#[cfg(windows)]
18use flate2::read::MultiGzDecoder;
19
20use crate::{Diagnostic, Document, RawDocument, diagnostics, ffi};
21
22#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
24#[derive(Clone, Debug, Default, Eq, PartialEq)]
25pub enum IncludePolicy {
26 #[default]
28 Deny,
29 SourceTree,
35 Root(PathBuf),
41}
42
43#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
45#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
46pub enum Compression {
47 #[default]
53 Auto,
54 Plain,
56 Zstd,
58}
59
60#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
62#[derive(Clone, Debug, Default, Eq, PartialEq)]
63pub struct ParseOptions {
64 pub includes: IncludePolicy,
66 pub compression: Compression,
68}
69
70#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct ParseReport {
74 pub document: Document,
76 pub diagnostics: Vec<Diagnostic>,
78}
79
80#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum ParseErrorKind {
84 InvalidPath,
86 Read,
88 Decompression,
90 Unsupported,
92 Parse,
94}
95
96#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
98#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct ParseError {
100 pub path: PathBuf,
102 pub kind: ParseErrorKind,
104 pub message: String,
106}
107
108impl fmt::Display for ParseError {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(formatter, "{}: {}", self.path.display(), self.message)
111 }
112}
113
114impl std::error::Error for ParseError {}
115
116#[derive(Clone, Debug, Default, Eq, PartialEq)]
123pub struct Parser {
124 options: ParseOptions,
125}
126
127impl Parser {
128 #[must_use]
130 pub const fn new(options: ParseOptions) -> Self {
131 Self { options }
132 }
133
134 #[must_use]
136 pub const fn options(&self) -> &ParseOptions {
137 &self.options
138 }
139
140 pub fn parse_file(&self, path: impl AsRef<Path>) -> Result<ParseReport, ParseError> {
152 let path = path.as_ref();
153 match self.options.compression {
154 Compression::Auto if path.extension().is_some_and(|extension| extension == "zst") => {
155 self.parse_zstd_file(path)
156 }
157 Compression::Auto => self.parse_auto_file(path),
158 Compression::Plain => {
159 let source = std::fs::read(path).map_err(|error| read_error(path, &error))?;
160 self.parse_plain_bytes(path, &source)
161 }
162 Compression::Zstd => self.parse_zstd_file(path),
163 }
164 }
165
166 pub fn parse_bytes(
178 &self,
179 source_path: impl AsRef<Path>,
180 source: &[u8],
181 ) -> Result<ParseReport, ParseError> {
182 let path = source_path.as_ref();
183 match self.options.compression {
184 Compression::Auto if has_zstd_magic(source) => self.parse_zstd_bytes(path, source),
185 Compression::Auto | Compression::Plain => self.parse_plain_bytes(path, source),
186 Compression::Zstd => self.parse_zstd_bytes(path, source),
187 }
188 }
189
190 fn parse_zstd_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
191 let source = File::open(path)
192 .and_then(zstd::stream::decode_all)
193 .map_err(|error| decompression_error(path, &error))?;
194 self.parse_plain_bytes(path, &source)
195 }
196
197 #[cfg(unix)]
198 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
199 self.parse_native_file(path)
200 }
201
202 #[cfg(windows)]
203 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
204 let source = File::open(path).map_err(|error| read_error(path, &error))?;
205 if path.extension().is_some_and(|extension| extension == "gz") {
206 let mut decoded = Vec::new();
207 MultiGzDecoder::new(source)
208 .read_to_end(&mut decoded)
209 .map_err(|error| gzip_decompression_error(path, &error))?;
210 self.parse_plain_bytes(path, &decoded)
211 } else {
212 let mut source = source;
213 let mut bytes = Vec::new();
214 source
215 .read_to_end(&mut bytes)
216 .map_err(|error| read_error(path, &error))?;
217 self.parse_plain_bytes(path, &bytes)
218 }
219 }
220
221 fn parse_zstd_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
222 let source =
223 zstd::stream::decode_all(source).map_err(|error| decompression_error(path, &error))?;
224 self.parse_plain_bytes(path, &source)
225 }
226
227 #[cfg(unix)]
228 fn parse_native_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
229 self.finish(path, |c_path, include_root, allow_includes| {
230 ffi::parse_file(c_path, include_root.map(CString::as_c_str), allow_includes)
231 })
232 }
233
234 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
235 self.finish(path, |c_path, include_root, allow_includes| {
236 ffi::parse_buffer(
237 c_path,
238 source,
239 include_root.map(CString::as_c_str),
240 allow_includes,
241 )
242 })
243 }
244
245 fn finish(
246 &self,
247 path: &Path,
248 parse: impl FnOnce(&CString, Option<&CString>, bool) -> Result<RawDocument, String>,
249 ) -> Result<ParseReport, ParseError> {
250 let c_path = path_label(path).map_err(|_| ParseError {
251 path: path.to_path_buf(),
252 kind: ParseErrorKind::InvalidPath,
253 message: "manual source path contains a NUL byte".into(),
254 })?;
255 let (include_root, allow_includes) = self.include_root()?;
256 let raw = parse(&c_path, include_root.as_ref(), allow_includes).map_err(|message| {
257 ParseError {
258 path: path.to_path_buf(),
259 kind: ParseErrorKind::Parse,
260 message,
261 }
262 })?;
263 Ok(ParseReport {
264 document: raw.document,
265 diagnostics: diagnostics::parse_diagnostics(&raw.diagnostics),
266 })
267 }
268
269 fn include_root(&self) -> Result<(Option<CString>, bool), ParseError> {
270 match &self.options.includes {
271 IncludePolicy::Deny => Ok((None, false)),
272 #[cfg(unix)]
273 IncludePolicy::SourceTree => Ok((None, true)),
274 #[cfg(windows)]
275 IncludePolicy::SourceTree => Err(unsupported_includes(PathBuf::new())),
276 IncludePolicy::Root(root) if root.as_os_str().is_empty() => Err(ParseError {
277 path: root.clone(),
278 kind: ParseErrorKind::InvalidPath,
279 message: "manual include root is empty".into(),
280 }),
281 #[cfg(unix)]
282 IncludePolicy::Root(root) => CString::new(root.as_os_str().as_bytes())
283 .map(Some)
284 .map(|root| (root, true))
285 .map_err(|_| ParseError {
286 path: root.clone(),
287 kind: ParseErrorKind::InvalidPath,
288 message: "manual include root contains a NUL byte".into(),
289 }),
290 #[cfg(windows)]
291 IncludePolicy::Root(root) => Err(unsupported_includes(root.clone())),
292 }
293 }
294}
295
296#[cfg(unix)]
297fn path_label(path: &Path) -> Result<CString, std::ffi::NulError> {
298 CString::new(path.as_os_str().as_bytes())
299}
300
301#[cfg(windows)]
302fn path_label(path: &Path) -> Result<CString, std::ffi::NulError> {
303 CString::new(path.to_string_lossy().as_bytes())
304}
305
306#[cfg(windows)]
307fn unsupported_includes(path: PathBuf) -> ParseError {
308 ParseError {
309 path,
310 kind: ParseErrorKind::Unsupported,
311 message:
312 "libmandoc file inclusion is unavailable on Windows; resolve .so sources before parsing"
313 .into(),
314 }
315}
316
317fn has_zstd_magic(source: &[u8]) -> bool {
318 source.starts_with(&[0x28, 0xb5, 0x2f, 0xfd])
319}
320
321fn read_error(path: &Path, error: &io::Error) -> ParseError {
322 ParseError {
323 path: path.to_path_buf(),
324 kind: ParseErrorKind::Read,
325 message: error.to_string(),
326 }
327}
328
329fn decompression_error(path: &Path, error: &io::Error) -> ParseError {
330 ParseError {
331 path: path.to_path_buf(),
332 kind: ParseErrorKind::Decompression,
333 message: format!("could not decompress zstd manual source: {error}"),
334 }
335}
336
337#[cfg(windows)]
338fn gzip_decompression_error(path: &Path, error: &io::Error) -> ParseError {
339 ParseError {
340 path: path.to_path_buf(),
341 kind: ParseErrorKind::Decompression,
342 message: format!("could not decompress gzip manual source: {error}"),
343 }
344}