1use std::{
4 ffi::CString,
5 fmt,
6 fs::File,
7 io,
8 os::unix::ffi::OsStrExt,
9 path::{Path, PathBuf},
10 sync::{Mutex, OnceLock},
11};
12
13use crate::{Diagnostic, Document, RawDocument, diagnostics, ffi};
14
15static PARSER_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
16
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
19#[derive(Clone, Debug, Default, Eq, PartialEq)]
20pub enum IncludePolicy {
21 #[default]
23 Deny,
24 SourceTree,
26 Root(PathBuf),
28}
29
30#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
33pub enum Compression {
34 #[default]
37 Auto,
38 Plain,
40 Zstd,
42}
43
44#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
46#[derive(Clone, Debug, Default, Eq, PartialEq)]
47pub struct ParseOptions {
48 pub includes: IncludePolicy,
49 pub compression: Compression,
50}
51
52#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct ParseReport {
56 pub document: Document,
57 pub diagnostics: Vec<Diagnostic>,
58}
59
60#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum ParseErrorKind {
64 InvalidPath,
65 Read,
66 Decompression,
67 Parse,
68}
69
70#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct ParseError {
74 pub path: PathBuf,
75 pub kind: ParseErrorKind,
76 pub message: String,
77}
78
79impl fmt::Display for ParseError {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(formatter, "{}: {}", self.path.display(), self.message)
82 }
83}
84
85impl std::error::Error for ParseError {}
86
87#[derive(Clone, Debug, Default, Eq, PartialEq)]
89pub struct Parser {
90 options: ParseOptions,
91}
92
93impl Parser {
94 #[must_use]
96 pub const fn new(options: ParseOptions) -> Self {
97 Self { options }
98 }
99
100 #[must_use]
102 pub const fn options(&self) -> &ParseOptions {
103 &self.options
104 }
105
106 pub fn parse_file(&self, path: impl AsRef<Path>) -> Result<ParseReport, ParseError> {
116 let path = path.as_ref();
117 match self.options.compression {
118 Compression::Auto if path.extension().is_some_and(|extension| extension == "zst") => {
119 self.parse_zstd_file(path)
120 }
121 Compression::Auto => self.parse_native_file(path),
122 Compression::Plain => {
123 let source = std::fs::read(path).map_err(|error| read_error(path, &error))?;
124 self.parse_plain_bytes(path, &source)
125 }
126 Compression::Zstd => self.parse_zstd_file(path),
127 }
128 }
129
130 pub fn parse_bytes(
141 &self,
142 source_path: impl AsRef<Path>,
143 source: &[u8],
144 ) -> Result<ParseReport, ParseError> {
145 let path = source_path.as_ref();
146 match self.options.compression {
147 Compression::Auto if has_zstd_magic(source) => self.parse_zstd_bytes(path, source),
148 Compression::Auto | Compression::Plain => self.parse_plain_bytes(path, source),
149 Compression::Zstd => self.parse_zstd_bytes(path, source),
150 }
151 }
152
153 fn parse_zstd_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
154 let source = File::open(path)
155 .and_then(zstd::stream::decode_all)
156 .map_err(|error| decompression_error(path, &error))?;
157 self.parse_plain_bytes(path, &source)
158 }
159
160 fn parse_zstd_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
161 let source =
162 zstd::stream::decode_all(source).map_err(|error| decompression_error(path, &error))?;
163 self.parse_plain_bytes(path, &source)
164 }
165
166 fn parse_native_file(&self, path: &Path) -> Result<ParseReport, ParseError> {
167 self.finish(path, |c_path, include_root, allow_includes| {
168 ffi::parse_file(c_path, include_root.map(CString::as_c_str), allow_includes)
169 })
170 }
171
172 fn parse_plain_bytes(&self, path: &Path, source: &[u8]) -> Result<ParseReport, ParseError> {
173 self.finish(path, |c_path, include_root, allow_includes| {
174 ffi::parse_buffer(
175 c_path,
176 source,
177 include_root.map(CString::as_c_str),
178 allow_includes,
179 )
180 })
181 }
182
183 fn finish(
184 &self,
185 path: &Path,
186 parse: impl FnOnce(&CString, Option<&CString>, bool) -> Result<RawDocument, String>,
187 ) -> Result<ParseReport, ParseError> {
188 let c_path = CString::new(path.as_os_str().as_bytes()).map_err(|_| ParseError {
189 path: path.to_path_buf(),
190 kind: ParseErrorKind::InvalidPath,
191 message: "manual source path contains a NUL byte".into(),
192 })?;
193 let lock = PARSER_LOCK.get_or_init(|| Mutex::new(()));
194 let _guard = lock
195 .lock()
196 .unwrap_or_else(std::sync::PoisonError::into_inner);
197 let (include_root, allow_includes) = self.include_root()?;
198 let raw = parse(&c_path, include_root.as_ref(), allow_includes).map_err(|message| {
199 ParseError {
200 path: path.to_path_buf(),
201 kind: ParseErrorKind::Parse,
202 message,
203 }
204 })?;
205 Ok(ParseReport {
206 document: raw.document,
207 diagnostics: diagnostics::parse_diagnostics(&raw.diagnostics),
208 })
209 }
210
211 fn include_root(&self) -> Result<(Option<CString>, bool), ParseError> {
212 match &self.options.includes {
213 IncludePolicy::Deny => Ok((None, false)),
214 IncludePolicy::SourceTree => Ok((None, true)),
215 IncludePolicy::Root(root) => CString::new(root.as_os_str().as_bytes())
216 .map(Some)
217 .map(|root| (root, true))
218 .map_err(|_| ParseError {
219 path: root.clone(),
220 kind: ParseErrorKind::InvalidPath,
221 message: "manual include root contains a NUL byte".into(),
222 }),
223 }
224 }
225}
226
227fn has_zstd_magic(source: &[u8]) -> bool {
228 source.starts_with(&[0x28, 0xb5, 0x2f, 0xfd])
229}
230
231fn read_error(path: &Path, error: &io::Error) -> ParseError {
232 ParseError {
233 path: path.to_path_buf(),
234 kind: ParseErrorKind::Read,
235 message: error.to_string(),
236 }
237}
238
239fn decompression_error(path: &Path, error: &io::Error) -> ParseError {
240 ParseError {
241 path: path.to_path_buf(),
242 kind: ParseErrorKind::Decompression,
243 message: format!("could not decompress zstd manual source: {error}"),
244 }
245}