use std::{
ffi::{CStr, CString},
fmt,
fs::File,
io,
path::{Path, PathBuf},
};
use crate::transport::{IncludeSettings, PreparedInput};
#[cfg(windows)]
use std::io::Read;
use crate::{
Diagnostic, DiagnosticLevel, Document, RawDocument, SourceBundle, compression, diagnostics, ffi,
};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum InputFormat {
#[default]
Auto,
Man,
Mdoc,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum IncludePolicy {
#[default]
Deny,
SourceTree,
Root(PathBuf),
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Compression {
#[default]
Auto,
Plain,
Zstd,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ParseOptions {
pub includes: IncludePolicy,
pub compression: Compression,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseReport {
pub document: Document,
pub diagnostics: Vec<Diagnostic>,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ParseErrorKind {
InvalidPath,
Read,
Decompression,
Unsupported,
Parse,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseError {
pub path: PathBuf,
pub kind: ParseErrorKind,
pub message: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.path.display(), self.message)
}
}
impl std::error::Error for ParseError {}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Parser {
options: ParseOptions,
input_format: InputFormat,
mdoc_operating_system: Option<CString>,
}
impl Parser {
#[must_use]
pub const fn new(options: ParseOptions) -> Self {
Self {
options,
input_format: InputFormat::Auto,
mdoc_operating_system: None,
}
}
#[must_use]
pub const fn options(&self) -> &ParseOptions {
&self.options
}
#[must_use]
pub const fn with_input_format(mut self, input_format: InputFormat) -> Self {
self.input_format = input_format;
self
}
#[must_use]
pub const fn input_format(&self) -> InputFormat {
self.input_format
}
pub fn with_mdoc_operating_system(
mut self,
operating_system: impl AsRef<str>,
) -> Result<Self, std::ffi::NulError> {
self.mdoc_operating_system = Some(CString::new(operating_system.as_ref())?);
Ok(self)
}
#[must_use]
pub fn mdoc_operating_system(&self) -> Option<&CStr> {
self.mdoc_operating_system.as_deref()
}
pub fn parse_file(&self, path: impl AsRef<Path>) -> Result<ParseReport, ParseError> {
let path = path.as_ref();
match self.options.compression {
Compression::Auto if path.extension().is_some_and(|extension| extension == "zst") => {
self.parse_zstd_file(path)
}
Compression::Auto => self.parse_auto_file(path),
Compression::Plain => {
let source = std::fs::read(path).map_err(|error| read_error(path, &error))?;
self.parse_plain_bytes(path, &source)
}
Compression::Zstd => self.parse_zstd_file(path),
}
}
pub fn parse_bytes(
&self,
source_path: impl AsRef<Path>,
source: &[u8],
) -> Result<ParseReport, ParseError> {
let path = source_path.as_ref();
let source = crate::transport::prepare_bytes(source, self.options.compression)
.map_err(|error| decompression_error(path, &error))?;
self.parse_plain_bytes(path, &source)
}
pub fn parse_bundle(
&self,
root: impl AsRef<Path>,
bundle: &SourceBundle,
) -> Result<ParseReport, ParseError> {
let root = root.as_ref();
let root_label = root.to_str().ok_or_else(|| ParseError {
path: root.to_path_buf(),
kind: ParseErrorKind::InvalidPath,
message: "source bundle roots must be UTF-8 logical paths".into(),
})?;
if bundle.get(root_label).is_none() {
return Err(ParseError {
path: root.to_path_buf(),
kind: ParseErrorKind::Read,
message: "source bundle does not contain the requested root".into(),
});
}
self.finish(root, |c_path, _| {
ffi::parse_bundle(
c_path,
bundle,
self.input_format,
self.mdoc_operating_system(),
)
})
}
fn parse_zstd_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
let source = File::open(path)
.and_then(compression::decode_zstd)
.map_err(|error| decompression_error(path, &error))?;
self.parse_plain_bytes(path, &source)
}
#[cfg(unix)]
fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
self.parse_native_file(path)
}
#[cfg(windows)]
fn parse_auto_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
let (source, gzip) =
compression::open_auto_file(path).map_err(|error| read_error(path, &error))?;
if gzip {
let decoded = compression::decode_gzip(source)
.map_err(|error| gzip_decompression_error(path, &error))?;
self.parse_plain_bytes(path, &decoded)
} else {
let mut source = source;
let mut bytes = Vec::new();
source
.read_to_end(&mut bytes)
.map_err(|error| read_error(path, &error))?;
self.parse_plain_bytes(path, &bytes)
}
}
#[cfg(unix)]
fn parse_native_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
self.finish(path, |c_path, includes| {
ffi::parse_file(
c_path,
includes.root.as_deref(),
includes.allow_includes,
self.input_format,
self.mdoc_operating_system(),
)
})
}
#[cfg(unix)]
fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
self.finish(path, |c_path, includes| {
ffi::parse_buffer(
c_path,
source,
includes.root.as_deref(),
includes.allow_includes,
self.input_format,
self.mdoc_operating_system(),
)
})
}
#[cfg(windows)]
fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
self.finish(path, |c_path, includes| {
ffi::parse_buffer(
c_path,
source,
includes.root.as_deref(),
includes.allow_includes,
self.input_format,
self.mdoc_operating_system(),
)
})
}
fn finish(
&self,
path: &Path,
parse: impl FnOnce(&CString, &IncludeSettings) -> Result<RawDocument, String>,
) -> Result<ParseReport, ParseError> {
let prepared = PreparedInput::new(path, &self.options.includes)?;
let raw = parse(&prepared.path, &prepared.includes).map_err(|message| ParseError {
path: path.to_path_buf(),
kind: ParseErrorKind::Parse,
message,
})?;
let mut findings = diagnostics::parse_diagnostics(&raw.diagnostics);
if raw.node_truncated {
findings.push(Diagnostic {
level: DiagnosticLevel::Warning,
message: diagnostics::SYNTAX_TREE_DEPTH_MESSAGE.into(),
location: None,
});
}
if raw.equation_truncated {
findings.push(Diagnostic {
level: DiagnosticLevel::Warning,
message: diagnostics::EQUATION_TREE_DEPTH_MESSAGE.into(),
location: None,
});
}
Ok(ParseReport {
document: raw.document,
diagnostics: findings,
})
}
}
fn read_error(path: &Path, error: &io::Error) -> ParseError {
ParseError {
path: path.to_path_buf(),
kind: ParseErrorKind::Read,
message: error.to_string(),
}
}
fn decompression_error(path: &Path, error: &io::Error) -> ParseError {
ParseError {
path: path.to_path_buf(),
kind: ParseErrorKind::Decompression,
message: format!("could not decompress zstd manual source: {error}"),
}
}
#[cfg(windows)]
fn gzip_decompression_error(path: &Path, error: &io::Error) -> ParseError {
ParseError {
path: path.to_path_buf(),
kind: ParseErrorKind::Decompression,
message: format!("could not decompress gzip manual source: {error}"),
}
}