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