1use std::{
4 ffi::{CStr, 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
17use crate::{
18 Diagnostic, DiagnosticLevel, Document, RawDocument, SourceBundle, compression, diagnostics, ffi,
19};
20
21#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub enum InputFormat {
25 #[default]
27 Auto,
28 Man,
30 Mdoc,
32}
33
34#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
36#[derive(Clone, Debug, Default, Eq, PartialEq)]
37pub enum IncludePolicy {
38 #[default]
40 Deny,
41 SourceTree,
47 Root(PathBuf),
54}
55
56#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub enum Compression {
60 #[default]
66 Auto,
67 Plain,
69 Zstd,
71}
72
73#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
75#[derive(Clone, Debug, Default, Eq, PartialEq)]
76pub struct ParseOptions {
77 pub includes: IncludePolicy,
79 pub compression: Compression,
81}
82
83#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct ParseReport {
87 pub document: Document,
89 pub diagnostics: Vec<Diagnostic>,
91}
92
93#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum ParseErrorKind {
97 InvalidPath,
99 Read,
101 Decompression,
103 Unsupported,
105 Parse,
107}
108
109#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct ParseError {
113 pub path: PathBuf,
115 pub kind: ParseErrorKind,
117 pub message: String,
119}
120
121impl fmt::Display for ParseError {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(formatter, "{}: {}", self.path.display(), self.message)
124 }
125}
126
127impl std::error::Error for ParseError {}
128
129#[derive(Clone, Debug, Default, Eq, PartialEq)]
137pub struct Parser {
138 options: ParseOptions,
139 input_format: InputFormat,
140 mdoc_operating_system: Option<CString>,
141}
142
143impl Parser {
144 #[must_use]
146 pub const fn new(options: ParseOptions) -> Self {
147 Self {
148 options,
149 input_format: InputFormat::Auto,
150 mdoc_operating_system: None,
151 }
152 }
153
154 #[must_use]
156 pub const fn options(&self) -> &ParseOptions {
157 &self.options
158 }
159
160 #[must_use]
162 pub const fn with_input_format(mut self, input_format: InputFormat) -> Self {
163 self.input_format = input_format;
164 self
165 }
166
167 #[must_use]
169 pub const fn input_format(&self) -> InputFormat {
170 self.input_format
171 }
172
173 pub fn with_mdoc_operating_system(
185 mut self,
186 operating_system: impl AsRef<str>,
187 ) -> Result<Self, std::ffi::NulError> {
188 self.mdoc_operating_system = Some(CString::new(operating_system.as_ref())?);
189 Ok(self)
190 }
191
192 #[must_use]
194 pub fn mdoc_operating_system(&self) -> Option<&CStr> {
195 self.mdoc_operating_system.as_deref()
196 }
197
198 pub fn parse_file(&self, path: impl AsRef<Path>) -> Result<ParseReport, ParseError> {
210 let path = path.as_ref();
211 match self.options.compression {
212 Compression::Auto if path.extension().is_some_and(|extension| extension == "zst") => {
213 self.parse_zstd_file(path)
214 }
215 Compression::Auto => self.parse_auto_file(path),
216 Compression::Plain => {
217 let source = std::fs::read(path).map_err(|error| read_error(path, &error))?;
218 self.parse_plain_bytes(path, &source)
219 }
220 Compression::Zstd => self.parse_zstd_file(path),
221 }
222 }
223
224 pub fn parse_bytes(
236 &self,
237 source_path: impl AsRef<Path>,
238 source: &[u8],
239 ) -> Result<ParseReport, ParseError> {
240 let path = source_path.as_ref();
241 match self.options.compression {
242 Compression::Auto if has_zstd_magic(source) => self.parse_zstd_bytes(path, source),
243 Compression::Auto | Compression::Plain => self.parse_plain_bytes(path, source),
244 Compression::Zstd => self.parse_zstd_bytes(path, source),
245 }
246 }
247
248 pub fn parse_bundle(
260 &self,
261 root: impl AsRef<Path>,
262 bundle: &SourceBundle,
263 ) -> Result<ParseReport, ParseError> {
264 let root = root.as_ref();
265 let root_label = root.to_str().ok_or_else(|| ParseError {
266 path: root.to_path_buf(),
267 kind: ParseErrorKind::InvalidPath,
268 message: "source bundle roots must be UTF-8 logical paths".into(),
269 })?;
270 if bundle.get(root_label).is_none() {
271 return Err(ParseError {
272 path: root.to_path_buf(),
273 kind: ParseErrorKind::Read,
274 message: "source bundle does not contain the requested root".into(),
275 });
276 }
277 self.finish(root, |c_path, _| {
278 ffi::parse_bundle(
279 c_path,
280 bundle,
281 self.input_format,
282 self.mdoc_operating_system(),
283 )
284 })
285 }
286
287 fn parse_zstd_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
288 let source = File::open(path)
289 .and_then(compression::decode_zstd)
290 .map_err(|error| decompression_error(path, &error))?;
291 self.parse_plain_bytes(path, &source)
292 }
293
294 #[cfg(unix)]
295 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
296 self.parse_native_file(path)
297 }
298
299 #[cfg(windows)]
300 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
301 let (source, gzip) =
302 compression::open_auto_file(path).map_err(|error| read_error(path, &error))?;
303 if gzip {
304 let decoded = compression::decode_gzip(source)
305 .map_err(|error| gzip_decompression_error(path, &error))?;
306 self.parse_plain_bytes(path, &decoded)
307 } else {
308 let mut source = source;
309 let mut bytes = Vec::new();
310 source
311 .read_to_end(&mut bytes)
312 .map_err(|error| read_error(path, &error))?;
313 self.parse_plain_bytes(path, &bytes)
314 }
315 }
316
317 fn parse_zstd_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
318 let source =
319 compression::decode_zstd(source).map_err(|error| decompression_error(path, &error))?;
320 self.parse_plain_bytes(path, &source)
321 }
322
323 #[cfg(unix)]
324 fn parse_native_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
325 self.finish(path, |c_path, includes| {
326 ffi::parse_file(
327 c_path,
328 includes.root.as_deref(),
329 includes.allow_includes,
330 self.input_format,
331 self.mdoc_operating_system(),
332 )
333 })
334 }
335
336 #[cfg(unix)]
337 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
338 self.finish(path, |c_path, includes| {
339 ffi::parse_buffer(
340 c_path,
341 source,
342 includes.root.as_deref(),
343 includes.allow_includes,
344 self.input_format,
345 self.mdoc_operating_system(),
346 )
347 })
348 }
349
350 #[cfg(windows)]
351 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
352 self.finish(path, |c_path, includes| {
353 ffi::parse_buffer(
354 c_path,
355 source,
356 includes.root.as_deref(),
357 includes.allow_includes,
358 self.input_format,
359 self.mdoc_operating_system(),
360 )
361 })
362 }
363
364 fn finish(
365 &self,
366 path: &Path,
367 parse: impl FnOnce(&CString, &IncludeSettings) -> Result<RawDocument, String>,
368 ) -> Result<ParseReport, ParseError> {
369 let c_path = path_label(path).map_err(|_| ParseError {
370 path: path.to_path_buf(),
371 kind: ParseErrorKind::InvalidPath,
372 message: "manual source path contains a NUL byte".into(),
373 })?;
374 let include_settings = self.include_settings(path)?;
375 let raw = parse(&c_path, &include_settings).map_err(|message| ParseError {
376 path: path.to_path_buf(),
377 kind: ParseErrorKind::Parse,
378 message,
379 })?;
380 let mut findings = diagnostics::parse_diagnostics(&raw.diagnostics);
381 if raw.node_truncated {
382 findings.push(Diagnostic {
383 level: DiagnosticLevel::Warning,
384 message: diagnostics::SYNTAX_TREE_DEPTH_MESSAGE.into(),
385 location: None,
386 });
387 }
388 if raw.equation_truncated {
389 findings.push(Diagnostic {
390 level: DiagnosticLevel::Warning,
391 message: diagnostics::EQUATION_TREE_DEPTH_MESSAGE.into(),
392 location: None,
393 });
394 }
395 Ok(ParseReport {
396 document: raw.document,
397 diagnostics: findings,
398 })
399 }
400
401 pub(crate) fn include_settings(
402 &self,
403 source_path: &Path,
404 ) -> Result<IncludeSettings, ParseError> {
405 #[cfg(unix)]
406 let _ = source_path;
407
408 match &self.options.includes {
409 IncludePolicy::Deny => Ok(IncludeSettings {
410 root: None,
411 allow_includes: false,
412 }),
413 #[cfg(unix)]
414 IncludePolicy::SourceTree => Ok(IncludeSettings {
415 root: None,
416 allow_includes: true,
417 }),
418 #[cfg(windows)]
419 IncludePolicy::SourceTree => Err(unsupported_includes(source_path.to_path_buf())),
420 IncludePolicy::Root(root) if root.as_os_str().is_empty() => Err(ParseError {
421 path: root.clone(),
422 kind: ParseErrorKind::InvalidPath,
423 message: "manual include root is empty".into(),
424 }),
425 #[cfg(unix)]
426 IncludePolicy::Root(root) => CString::new(root.as_os_str().as_bytes())
427 .map(|root| IncludeSettings {
428 root: Some(root),
429 allow_includes: true,
430 })
431 .map_err(|_| ParseError {
432 path: root.clone(),
433 kind: ParseErrorKind::InvalidPath,
434 message: "manual include root contains a NUL byte".into(),
435 }),
436 #[cfg(windows)]
437 IncludePolicy::Root(root) => Ok(IncludeSettings {
438 root: Some(root.clone()),
439 allow_includes: true,
440 }),
441 }
442 }
443}
444
445pub(crate) struct IncludeSettings {
446 #[cfg(unix)]
447 pub(crate) root: Option<CString>,
448 #[cfg(windows)]
449 pub(crate) root: Option<PathBuf>,
450 pub(crate) allow_includes: bool,
451}
452
453#[cfg(unix)]
454pub(crate) fn path_label(path: &Path) -> Result<CString, std::ffi::NulError> {
455 CString::new(path.as_os_str().as_bytes())
456}
457
458#[cfg(windows)]
459pub(crate) fn path_label(path: &Path) -> Result<CString, std::ffi::NulError> {
460 CString::new(path.to_string_lossy().as_bytes())
461}
462
463#[cfg(windows)]
464fn unsupported_includes(path: PathBuf) -> ParseError {
465 ParseError {
466 path,
467 kind: ParseErrorKind::Unsupported,
468 message: "libmandoc-compatible source-tree inclusion is unavailable on Windows; use IncludePolicy::Root or SourceBundle"
469 .into(),
470 }
471}
472
473fn has_zstd_magic(source: &[u8]) -> bool {
474 source.starts_with(&[0x28, 0xb5, 0x2f, 0xfd])
475}
476
477fn read_error(path: &Path, error: &io::Error) -> ParseError {
478 ParseError {
479 path: path.to_path_buf(),
480 kind: ParseErrorKind::Read,
481 message: error.to_string(),
482 }
483}
484
485fn decompression_error(path: &Path, error: &io::Error) -> ParseError {
486 ParseError {
487 path: path.to_path_buf(),
488 kind: ParseErrorKind::Decompression,
489 message: format!("could not decompress zstd manual source: {error}"),
490 }
491}
492
493#[cfg(windows)]
494fn gzip_decompression_error(path: &Path, error: &io::Error) -> ParseError {
495 ParseError {
496 path: path.to_path_buf(),
497 kind: ParseErrorKind::Decompression,
498 message: format!("could not decompress gzip manual source: {error}"),
499 }
500}