1#![deny(missing_docs)]
3#![forbid(unsafe_code)]
4
5use crate::GenerationPlan;
6use boxology_generator::{GeneratedTree, OUTPUTS};
7use boxology_generator_model::Diagnostics;
8use boxology_generator_model::GenerationRequest;
9use boxology_generator_writer::WriteError;
10use boxology_manifest::RelativePath;
11use std::{
12 fmt, fs, io,
13 path::{Path, PathBuf},
14};
15
16type Rule = (&'static str, &'static str, &'static str);
17const SOURCE: &str = "specs/s5-manifest-and-validation.md D5";
18const INFERRED_SOURCE: &str = "specs/s5-manifest-and-validation.md D5 (inferred)";
19const INPUT_TEXT: &str = "a generation input must be a readable regular file";
20const GENERATOR_TEXT: &str = "the contract generator returned diagnostics";
21const WRITER_TEXT: &str = "the generated tree could not be written";
22const COVERAGE_TEXT: &str = "a generator output is not covered by a declared output pattern";
23const SCHEMA_TEXT: &str = "the checked-in schema document must be a readable regular file";
24const INPUT: Rule = ("BXW0070", INPUT_TEXT, SOURCE);
25const GENERATOR: Rule = ("BXW0071", GENERATOR_TEXT, SOURCE);
26const WRITER: Rule = ("BXW0072", WRITER_TEXT, SOURCE);
27const COVERAGE: Rule = ("BXW0073", COVERAGE_TEXT, INFERRED_SOURCE);
30const SCHEMA_FILE: Rule = ("BXW0076", SCHEMA_TEXT, INFERRED_SOURCE);
33
34#[derive(Debug, Eq, PartialEq)]
36pub struct Outcome {
37 written: Vec<String>,
38 removed: Vec<String>,
39 base_schema: Option<Vec<u8>>,
40 submitted_schema: Vec<u8>,
41}
42
43impl Outcome {
44 pub fn written(&self) -> &[String] {
46 &self.written
47 }
48
49 pub fn removed(&self) -> &[String] {
51 &self.removed
52 }
53
54 pub fn is_unchanged(&self) -> bool {
56 self.written.is_empty() && self.removed.is_empty()
57 }
58
59 pub fn base_schema(&self) -> Option<&[u8]> {
61 self.base_schema.as_deref()
62 }
63
64 pub fn submitted_schema(&self) -> &[u8] {
66 &self.submitted_schema
67 }
68}
69
70#[derive(Debug)]
72pub struct ExecuteError {
73 code: &'static str,
74 location: PathBuf,
75 detail: &'static str,
76 cause: Cause,
77}
78
79#[derive(Debug)]
80enum Cause {
81 Input,
82 Generator(Diagnostics),
83 Writer(WriteError),
84 Coverage,
85 Schema,
86}
87
88impl ExecuteError {
89 pub fn code(&self) -> &'static str {
91 self.code
92 }
93
94 pub fn location(&self) -> &Path {
96 &self.location
97 }
98
99 pub fn path(&self) -> &Path {
101 self.location()
102 }
103
104 pub fn detail(&self) -> &'static str {
106 self.detail
107 }
108
109 pub fn diagnostics(&self) -> Option<&Diagnostics> {
111 match &self.cause {
112 Cause::Generator(diagnostics) => Some(diagnostics),
113 Cause::Input | Cause::Writer(_) | Cause::Coverage | Cause::Schema => None,
114 }
115 }
116
117 pub fn write_error(&self) -> Option<&WriteError> {
119 match &self.cause {
120 Cause::Writer(error) => Some(error),
121 Cause::Input | Cause::Generator(_) | Cause::Coverage | Cause::Schema => None,
122 }
123 }
124
125 fn input(path: PathBuf) -> Self {
126 Self {
127 code: INPUT.0,
128 location: path,
129 detail: INPUT.1,
130 cause: Cause::Input,
131 }
132 }
133
134 fn generator(path: PathBuf, diagnostics: Diagnostics) -> Self {
135 Self {
136 code: GENERATOR.0,
137 location: path,
138 detail: GENERATOR.1,
139 cause: Cause::Generator(diagnostics),
140 }
141 }
142
143 fn writer(path: PathBuf, error: WriteError) -> Self {
144 Self {
145 code: WRITER.0,
146 location: path,
147 detail: WRITER.1,
148 cause: Cause::Writer(error),
149 }
150 }
151
152 fn coverage(path: PathBuf) -> Self {
153 Self {
154 code: COVERAGE.0,
155 location: path,
156 detail: COVERAGE.1,
157 cause: Cause::Coverage,
158 }
159 }
160
161 fn schema(path: PathBuf) -> Self {
162 Self {
163 code: SCHEMA_FILE.0,
164 location: path,
165 detail: SCHEMA_FILE.1,
166 cause: Cause::Schema,
167 }
168 }
169}
170
171impl fmt::Display for ExecuteError {
172 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(
174 formatter,
175 "{} {:?}: {}",
176 self.code, self.location, self.detail
177 )?;
178 match &self.cause {
179 Cause::Generator(diagnostics) => write!(formatter, ": {diagnostics}"),
180 Cause::Writer(error) => write!(formatter, ": {error}"),
181 Cause::Input | Cause::Coverage | Cause::Schema => Ok(()),
182 }
183 }
184}
185
186impl std::error::Error for ExecuteError {
187 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
188 match &self.cause {
189 Cause::Writer(error) => Some(error),
190 Cause::Input | Cause::Generator(_) | Cause::Coverage | Cause::Schema => None,
191 }
192 }
193}
194
195#[derive(Debug)]
204pub struct ExecutePlans<'a, I> {
205 root: &'a Path,
206 plans: I,
207 terminal: bool,
208}
209
210impl<'a, I> Iterator for ExecutePlans<'a, I>
211where
212 I: Iterator<Item = &'a GenerationPlan>,
213{
214 type Item = Result<(&'a GenerationPlan, Outcome), ExecuteError>;
215
216 fn next(&mut self) -> Option<Self::Item> {
217 if self.terminal {
218 return None;
219 }
220 let plan = self.plans.next()?;
221 match execute(self.root, plan) {
222 Ok(outcome) => Some(Ok((plan, outcome))),
223 Err(error) => {
224 self.terminal = true;
225 Some(Err(error))
226 }
227 }
228 }
229}
230
231pub fn execute_plans<'a, I>(root: &'a Path, plans: I) -> ExecutePlans<'a, I::IntoIter>
239where
240 I: IntoIterator<Item = &'a GenerationPlan>,
241{
242 ExecutePlans {
243 root,
244 plans: plans.into_iter(),
245 terminal: false,
246 }
247}
248
249pub fn execute(root: &Path, plan: &GenerationPlan) -> Result<Outcome, ExecuteError> {
262 let package_root = plan.package_root().map_or("", RelativePath::as_str);
263 let (package_dir, tree) = generate_tree(root, plan)?;
264 let submitted_schema = tree
265 .files()
266 .iter()
267 .find(|file| file.path() == package_schema_path(plan))
268 .expect("generator outputs include schema.json")
269 .bytes()
270 .to_vec();
271 let base_schema = read_base_schema(root, plan)?;
272 guarded(root, package_root, false)?;
273 let changes = boxology_generator_writer::write(&package_dir, &tree, plan.outputs())
274 .map_err(|error| ExecuteError::writer(package_dir, error))?;
275 let mut written = changes.written;
276 let mut removed = changes.removed;
277 written.sort_unstable_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
278 removed.sort_unstable_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
279 Ok(Outcome {
280 written,
281 removed,
282 base_schema,
283 submitted_schema,
284 })
285}
286
287pub(crate) fn generate_tree(
288 root: &Path,
289 plan: &GenerationPlan,
290) -> Result<(PathBuf, GeneratedTree), ExecuteError> {
291 let package_root = plan.package_root().map_or("", RelativePath::as_str);
292 let package_dir = guarded(root, package_root, false)?;
293 let mut input_paths = plan.inputs().to_vec();
294 input_paths.sort_unstable();
295 let guarded_inputs = input_paths
296 .into_iter()
297 .map(|input| guarded(&package_dir, input.as_str(), true).map(|path| (input, path)))
298 .collect::<Result<Vec<_>, ExecuteError>>()?;
299 let mut inputs = guarded_inputs
300 .into_iter()
301 .map(|(input, path)| {
302 let bytes = fs::read(&path).map_err(|_| ExecuteError::input(path))?;
303 Ok((input.as_str().to_owned(), bytes))
304 })
305 .collect::<Result<Vec<_>, ExecuteError>>()?;
306 let raw_imports = plan
307 .imports()
308 .iter()
309 .map(|import| {
310 (
311 import.package().clone(),
312 import.schema().as_str().to_owned(),
313 )
314 })
315 .collect::<Vec<_>>();
316 for import in plan.imports() {
317 let schema = import.schema().clone();
318 let path = guarded(root, schema.as_str(), true)?;
319 let bytes = fs::read(&path).map_err(|_| ExecuteError::input(path))?;
320 inputs.push((schema.as_str().to_owned(), bytes));
321 }
322 let request = GenerationRequest::new(
323 plan.package_id().clone(),
324 plan.crate_root().as_str().to_owned(),
325 inputs,
326 raw_imports,
327 OUTPUTS.iter().map(|path| (*path).to_owned()).collect(),
328 )
329 .map_err(|diagnostics| {
330 ExecuteError::generator(package_dir.join(plan.crate_root().as_str()), diagnostics)
331 })?;
332 let tree = boxology_generator::generate(request).map_err(|diagnostics| {
333 ExecuteError::generator(package_dir.join(plan.crate_root().as_str()), diagnostics)
334 })?;
335 for output in OUTPUTS {
336 let output =
337 RelativePath::new(output.to_owned()).expect("generator outputs are valid paths");
338 if !plan
339 .outputs()
340 .iter()
341 .any(|pattern| pattern.matches(&output))
342 {
343 return Err(ExecuteError::coverage(package_dir.join(output.as_str())));
344 }
345 }
346 Ok((package_dir, tree))
347}
348
349fn read_base_schema(root: &Path, plan: &GenerationPlan) -> Result<Option<Vec<u8>>, ExecuteError> {
350 read_optional_file(root, plan.schema_path())
351}
352
353pub(crate) fn missing_schema(root: &Path, plan: &GenerationPlan) -> ExecuteError {
354 ExecuteError::schema(root.join(plan.schema_path().as_str()))
355}
356
357pub(crate) fn read_optional_file(
358 root: &Path,
359 logical: &RelativePath,
360) -> Result<Option<Vec<u8>>, ExecuteError> {
361 let location = root.join(logical.as_str());
362 match fs::symlink_metadata(&location) {
363 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
364 Err(_) | Ok(_) => {
365 let path = guarded(root, logical.as_str(), true)
366 .map_err(|error| ExecuteError::schema(error.location().to_path_buf()))?;
367 let bytes = fs::read(&path).map_err(|_| ExecuteError::schema(path))?;
368 Ok(Some(bytes))
369 }
370 }
371}
372
373fn package_schema_path(plan: &GenerationPlan) -> &str {
374 plan.package_root().map_or_else(
375 || plan.schema_path().as_str(),
376 |root| {
377 plan.schema_path()
378 .as_str()
379 .strip_prefix(root.as_str())
380 .and_then(|path| path.strip_prefix('/'))
381 .expect("the plan's schema is inside its package root")
382 },
383 )
384}
385
386pub(crate) fn guarded(root: &Path, relative: &str, file: bool) -> Result<PathBuf, ExecuteError> {
387 let mut path = root.to_owned();
388 let mut parts = relative
389 .split('/')
390 .filter(|part| !part.is_empty())
391 .peekable();
392 loop {
393 let metadata =
394 fs::symlink_metadata(&path).map_err(|_| ExecuteError::input(path.clone()))?;
395 let accepted = if parts.peek().is_none() && file {
396 metadata.is_file()
397 } else {
398 metadata.is_dir()
399 };
400 if metadata.file_type().is_symlink() || !accepted {
401 return Err(ExecuteError::input(path));
402 }
403 let Some(part) = parts.next() else {
404 return Ok(path);
405 };
406 path.push(part);
407 }
408}