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,
57 pub compression: Compression,
58}
59
60#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct ParseReport {
64 pub document: Document,
65 pub diagnostics: Vec<Diagnostic>,
66}
67
68#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum ParseErrorKind {
72 InvalidPath,
73 Read,
74 Decompression,
75 Unsupported,
76 Parse,
77}
78
79#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct ParseError {
83 pub path: PathBuf,
84 pub kind: ParseErrorKind,
85 pub message: String,
86}
87
88impl fmt::Display for ParseError {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(formatter, "{}: {}", self.path.display(), self.message)
91 }
92}
93
94impl std::error::Error for ParseError {}
95
96#[derive(Clone, Debug, Default, Eq, PartialEq)]
98pub struct Parser {
99 options: ParseOptions,
100}
101
102impl Parser {
103 #[must_use]
105 pub const fn new(options: ParseOptions) -> Self {
106 Self { options }
107 }
108
109 #[must_use]
111 pub const fn options(&self) -> &ParseOptions {
112 &self.options
113 }
114
115 pub fn parse_file(&self, path: impl AsRef<Path>) -> Result<ParseReport, ParseError> {
125 let path = path.as_ref();
126 match self.options.compression {
127 Compression::Auto if path.extension().is_some_and(|extension| extension == "zst") => {
128 self.parse_zstd_file(path)
129 }
130 Compression::Auto => self.parse_auto_file(path),
131 Compression::Plain => {
132 let source = std::fs::read(path).map_err(|error| read_error(path, &error))?;
133 self.parse_plain_bytes(path, &source)
134 }
135 Compression::Zstd => self.parse_zstd_file(path),
136 }
137 }
138
139 pub fn parse_bytes(
150 &self,
151 source_path: impl AsRef<Path>,
152 source: &[u8],
153 ) -> Result<ParseReport, ParseError> {
154 let path = source_path.as_ref();
155 match self.options.compression {
156 Compression::Auto if has_zstd_magic(source) => self.parse_zstd_bytes(path, source),
157 Compression::Auto | Compression::Plain => self.parse_plain_bytes(path, source),
158 Compression::Zstd => self.parse_zstd_bytes(path, source),
159 }
160 }
161
162 fn parse_zstd_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
163 let source = File::open(path)
164 .and_then(zstd::stream::decode_all)
165 .map_err(|error| decompression_error(path, &error))?;
166 self.parse_plain_bytes(path, &source)
167 }
168
169 #[cfg(unix)]
170 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
171 self.parse_native_file(path)
172 }
173
174 #[cfg(windows)]
175 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
176 let source = File::open(path).map_err(|error| read_error(path, &error))?;
177 if path.extension().is_some_and(|extension| extension == "gz") {
178 let mut decoded = Vec::new();
179 MultiGzDecoder::new(source)
180 .read_to_end(&mut decoded)
181 .map_err(|error| gzip_decompression_error(path, &error))?;
182 self.parse_plain_bytes(path, &decoded)
183 } else {
184 let mut source = source;
185 let mut bytes = Vec::new();
186 source
187 .read_to_end(&mut bytes)
188 .map_err(|error| read_error(path, &error))?;
189 self.parse_plain_bytes(path, &bytes)
190 }
191 }
192
193 fn parse_zstd_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
194 let source =
195 zstd::stream::decode_all(source).map_err(|error| decompression_error(path, &error))?;
196 self.parse_plain_bytes(path, &source)
197 }
198
199 #[cfg(unix)]
200 fn parse_native_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
201 self.finish(path, |c_path, include_root, allow_includes| {
202 ffi::parse_file(c_path, include_root.map(CString::as_c_str), allow_includes)
203 })
204 }
205
206 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
207 self.finish(path, |c_path, include_root, allow_includes| {
208 ffi::parse_buffer(
209 c_path,
210 source,
211 include_root.map(CString::as_c_str),
212 allow_includes,
213 )
214 })
215 }
216
217 fn finish(
218 &self,
219 path: &Path,
220 parse: impl FnOnce(&CString, Option<&CString>, bool) -> Result<RawDocument, String>,
221 ) -> Result<ParseReport, ParseError> {
222 let c_path = path_label(path).map_err(|_| ParseError {
223 path: path.to_path_buf(),
224 kind: ParseErrorKind::InvalidPath,
225 message: "manual source path contains a NUL byte".into(),
226 })?;
227 let lock = PARSER_LOCK.get_or_init(|| Mutex::new(()));
228 let _guard = lock
229 .lock()
230 .unwrap_or_else(std::sync::PoisonError::into_inner);
231 let (include_root, allow_includes) = self.include_root()?;
232 let raw = parse(&c_path, include_root.as_ref(), allow_includes).map_err(|message| {
233 ParseError {
234 path: path.to_path_buf(),
235 kind: ParseErrorKind::Parse,
236 message,
237 }
238 })?;
239 Ok(ParseReport {
240 document: raw.document,
241 diagnostics: diagnostics::parse_diagnostics(&raw.diagnostics),
242 })
243 }
244
245 fn include_root(&self) -> Result<(Option<CString>, bool), ParseError> {
246 match &self.options.includes {
247 IncludePolicy::Deny => Ok((None, false)),
248 #[cfg(unix)]
249 IncludePolicy::SourceTree => Ok((None, true)),
250 #[cfg(windows)]
251 IncludePolicy::SourceTree => Err(unsupported_includes(PathBuf::new())),
252 IncludePolicy::Root(root) if root.as_os_str().is_empty() => Err(ParseError {
253 path: root.clone(),
254 kind: ParseErrorKind::InvalidPath,
255 message: "manual include root is empty".into(),
256 }),
257 #[cfg(unix)]
258 IncludePolicy::Root(root) => CString::new(root.as_os_str().as_bytes())
259 .map(Some)
260 .map(|root| (root, true))
261 .map_err(|_| ParseError {
262 path: root.clone(),
263 kind: ParseErrorKind::InvalidPath,
264 message: "manual include root contains a NUL byte".into(),
265 }),
266 #[cfg(windows)]
267 IncludePolicy::Root(root) => Err(unsupported_includes(root.clone())),
268 }
269 }
270}
271
272#[cfg(unix)]
273fn path_label(path: &Path) -> Result<CString, std::ffi::NulError> {
274 CString::new(path.as_os_str().as_bytes())
275}
276
277#[cfg(windows)]
278fn path_label(path: &Path) -> Result<CString, std::ffi::NulError> {
279 CString::new(path.to_string_lossy().as_bytes())
280}
281
282#[cfg(windows)]
283fn unsupported_includes(path: PathBuf) -> ParseError {
284 ParseError {
285 path,
286 kind: ParseErrorKind::Unsupported,
287 message:
288 "libmandoc file inclusion is unavailable on Windows; resolve .so sources before parsing"
289 .into(),
290 }
291}
292
293fn has_zstd_magic(source: &[u8]) -> bool {
294 source.starts_with(&[0x28, 0xb5, 0x2f, 0xfd])
295}
296
297fn read_error(path: &Path, error: &io::Error) -> ParseError {
298 ParseError {
299 path: path.to_path_buf(),
300 kind: ParseErrorKind::Read,
301 message: error.to_string(),
302 }
303}
304
305fn decompression_error(path: &Path, error: &io::Error) -> ParseError {
306 ParseError {
307 path: path.to_path_buf(),
308 kind: ParseErrorKind::Decompression,
309 message: format!("could not decompress zstd manual source: {error}"),
310 }
311}
312
313#[cfg(windows)]
314fn gzip_decompression_error(path: &Path, error: &io::Error) -> ParseError {
315 ParseError {
316 path: path.to_path_buf(),
317 kind: ParseErrorKind::Decompression,
318 message: format!("could not decompress gzip manual source: {error}"),
319 }
320}