1use std::{
4 ffi::{CStr, CString},
5 fmt,
6 fs::File,
7 io,
8 path::{Path, PathBuf},
9};
10
11use crate::transport::{IncludeSettings, PreparedInput};
12
13#[cfg(windows)]
14use std::io::Read;
15
16use crate::{
17 Diagnostic, DiagnosticLevel, Document, RawDocument, SourceBundle, compression, diagnostics, ffi,
18};
19
20#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
22#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub enum InputFormat {
24 #[default]
26 Auto,
27 Man,
29 Mdoc,
31}
32
33#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
35#[derive(Clone, Debug, Default, Eq, PartialEq)]
36pub enum IncludePolicy {
37 #[default]
39 Deny,
40 SourceTree,
46 Root(PathBuf),
53}
54
55#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
57#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
58pub enum Compression {
59 #[default]
65 Auto,
66 Plain,
68 Zstd,
70}
71
72#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
74#[derive(Clone, Debug, Default, Eq, PartialEq)]
75pub struct ParseOptions {
76 pub includes: IncludePolicy,
78 pub compression: Compression,
80}
81
82#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
84#[derive(Clone, Debug, Eq, PartialEq)]
85pub struct ParseReport {
86 pub document: Document,
88 pub diagnostics: Vec<Diagnostic>,
90}
91
92#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum ParseErrorKind {
96 InvalidPath,
98 Read,
100 Decompression,
102 Unsupported,
104 Parse,
106}
107
108#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct ParseError {
112 pub path: PathBuf,
114 pub kind: ParseErrorKind,
116 pub message: String,
118}
119
120impl fmt::Display for ParseError {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(formatter, "{}: {}", self.path.display(), self.message)
123 }
124}
125
126impl std::error::Error for ParseError {}
127
128#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct Parser {
139 options: ParseOptions,
140 input_format: InputFormat,
141 mdoc_operating_system: Option<CString>,
142}
143
144impl Parser {
145 #[must_use]
147 pub const fn new(options: ParseOptions) -> Self {
148 Self {
149 options,
150 input_format: InputFormat::Auto,
151 mdoc_operating_system: None,
152 }
153 }
154
155 #[must_use]
157 pub const fn options(&self) -> &ParseOptions {
158 &self.options
159 }
160
161 #[must_use]
163 pub const fn with_input_format(mut self, input_format: InputFormat) -> Self {
164 self.input_format = input_format;
165 self
166 }
167
168 #[must_use]
170 pub const fn input_format(&self) -> InputFormat {
171 self.input_format
172 }
173
174 pub fn with_mdoc_operating_system(
186 mut self,
187 operating_system: impl AsRef<str>,
188 ) -> Result<Self, std::ffi::NulError> {
189 self.mdoc_operating_system = Some(CString::new(operating_system.as_ref())?);
190 Ok(self)
191 }
192
193 #[must_use]
195 pub fn mdoc_operating_system(&self) -> Option<&CStr> {
196 self.mdoc_operating_system.as_deref()
197 }
198
199 pub fn parse_file(&self, path: impl AsRef<Path>) -> Result<ParseReport, ParseError> {
211 let path = path.as_ref();
212 match self.options.compression {
213 Compression::Auto if path.extension().is_some_and(|extension| extension == "zst") => {
214 self.parse_zstd_file(path)
215 }
216 Compression::Auto => self.parse_auto_file(path),
217 Compression::Plain => {
218 let source = std::fs::read(path).map_err(|error| read_error(path, &error))?;
219 self.parse_plain_bytes(path, &source)
220 }
221 Compression::Zstd => self.parse_zstd_file(path),
222 }
223 }
224
225 pub fn parse_bytes(
237 &self,
238 source_path: impl AsRef<Path>,
239 source: &[u8],
240 ) -> Result<ParseReport, ParseError> {
241 let path = source_path.as_ref();
242 let source = crate::transport::prepare_bytes(source, self.options.compression)
243 .map_err(|error| decompression_error(path, &error))?;
244 self.parse_plain_bytes(path, &source)
245 }
246
247 pub fn parse_bundle(
259 &self,
260 root: impl AsRef<Path>,
261 bundle: &SourceBundle,
262 ) -> Result<ParseReport, ParseError> {
263 let root = root.as_ref();
264 let root_label = root.to_str().ok_or_else(|| ParseError {
265 path: root.to_path_buf(),
266 kind: ParseErrorKind::InvalidPath,
267 message: "source bundle roots must be UTF-8 logical paths".into(),
268 })?;
269 if bundle.get(root_label).is_none() {
270 return Err(ParseError {
271 path: root.to_path_buf(),
272 kind: ParseErrorKind::Read,
273 message: "source bundle does not contain the requested root".into(),
274 });
275 }
276 self.finish(root, |c_path, _| {
277 ffi::parse_bundle(
278 c_path,
279 bundle,
280 self.input_format,
281 self.mdoc_operating_system(),
282 )
283 })
284 }
285
286 fn parse_zstd_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
287 let source = File::open(path)
288 .and_then(compression::decode_zstd)
289 .map_err(|error| decompression_error(path, &error))?;
290 self.parse_plain_bytes(path, &source)
291 }
292
293 #[cfg(unix)]
294 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
295 self.parse_native_file(path)
296 }
297
298 #[cfg(windows)]
299 fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
300 let (source, gzip) =
301 compression::open_auto_file(path).map_err(|error| read_error(path, &error))?;
302 if gzip {
303 let decoded = compression::decode_gzip(source)
304 .map_err(|error| gzip_decompression_error(path, &error))?;
305 self.parse_plain_bytes(path, &decoded)
306 } else {
307 let mut source = source;
308 let mut bytes = Vec::new();
309 source
310 .read_to_end(&mut bytes)
311 .map_err(|error| read_error(path, &error))?;
312 self.parse_plain_bytes(path, &bytes)
313 }
314 }
315
316 #[cfg(unix)]
317 fn parse_native_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
318 self.finish(path, |c_path, includes| {
319 ffi::parse_file(
320 c_path,
321 includes.root.as_deref(),
322 includes.allow_includes,
323 self.input_format,
324 self.mdoc_operating_system(),
325 )
326 })
327 }
328
329 #[cfg(unix)]
330 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
331 self.finish(path, |c_path, includes| {
332 ffi::parse_buffer(
333 c_path,
334 source,
335 includes.root.as_deref(),
336 includes.allow_includes,
337 self.input_format,
338 self.mdoc_operating_system(),
339 )
340 })
341 }
342
343 #[cfg(windows)]
344 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
345 self.finish(path, |c_path, includes| {
346 ffi::parse_buffer(
347 c_path,
348 source,
349 includes.root.as_deref(),
350 includes.allow_includes,
351 self.input_format,
352 self.mdoc_operating_system(),
353 )
354 })
355 }
356
357 fn finish(
358 &self,
359 path: &Path,
360 parse: impl FnOnce(&CString, &IncludeSettings) -> Result<RawDocument, String>,
361 ) -> Result<ParseReport, ParseError> {
362 let prepared = PreparedInput::new(path, &self.options.includes)?;
363 let raw = parse(&prepared.path, &prepared.includes).map_err(|message| ParseError {
364 path: path.to_path_buf(),
365 kind: ParseErrorKind::Parse,
366 message,
367 })?;
368 let mut findings = diagnostics::parse_diagnostics(&raw.diagnostics);
369 if raw.node_truncated {
370 findings.push(Diagnostic {
371 level: DiagnosticLevel::Warning,
372 message: diagnostics::SYNTAX_TREE_DEPTH_MESSAGE.into(),
373 location: None,
374 });
375 }
376 if raw.equation_truncated {
377 findings.push(Diagnostic {
378 level: DiagnosticLevel::Warning,
379 message: diagnostics::EQUATION_TREE_DEPTH_MESSAGE.into(),
380 location: None,
381 });
382 }
383 Ok(ParseReport {
384 document: raw.document,
385 diagnostics: findings,
386 })
387 }
388}
389
390fn read_error(path: &Path, error: &io::Error) -> ParseError {
391 ParseError {
392 path: path.to_path_buf(),
393 kind: ParseErrorKind::Read,
394 message: error.to_string(),
395 }
396}
397
398fn decompression_error(path: &Path, error: &io::Error) -> ParseError {
399 ParseError {
400 path: path.to_path_buf(),
401 kind: ParseErrorKind::Decompression,
402 message: format!("could not decompress zstd manual source: {error}"),
403 }
404}
405
406#[cfg(windows)]
407fn gzip_decompression_error(path: &Path, error: &io::Error) -> ParseError {
408 ParseError {
409 path: path.to_path_buf(),
410 kind: ParseErrorKind::Decompression,
411 message: format!("could not decompress gzip manual source: {error}"),
412 }
413}