#![deny(missing_docs)]
#![forbid(unsafe_code)]
use crate::{
GenerationPlan, PlanError,
execute::{ExecuteError, generate_tree, guarded, read_optional_file},
plan,
};
use boxology_contract::BoxId;
use boxology_generator::GeneratedTree;
use boxology_manifest::RelativePath;
use boxology_workspace::{Completion, SelectedSchema, Workspace};
use std::{fmt, fs, io, path::Path};
type Rule = (&'static str, &'static str, &'static str);
const COMPARE_SOURCE: &str = "specs/s5-manifest-and-validation.md D6; boxology-details/08-rust-build-topology.md workspace operations and validation baseline step 2";
const COMPARE_TEXT: &str = "a checked-in derived artifact must be byte-identical to regeneration; regenerate the accountable package with boxology generate --package <id>";
const REGENERATION: Rule = ("BXW0083", COMPARE_TEXT, COMPARE_SOURCE);
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum DifferenceKind {
Missing,
Differing,
Stale,
}
impl DifferenceKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Missing => "missing",
Self::Differing => "differing",
Self::Stale => "stale",
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct CompareDifference {
package: BoxId,
path: RelativePath,
kind: DifferenceKind,
}
impl CompareDifference {
pub fn package(&self) -> &BoxId {
&self.package
}
pub fn path(&self) -> &RelativePath {
&self.path
}
pub fn kind(&self) -> DifferenceKind {
self.kind
}
pub fn code(&self) -> &'static str {
REGENERATION.0
}
pub fn detail(&self) -> &'static str {
REGENERATION.1
}
pub fn repair_command(&self) -> String {
format!("boxology generate --package {}", self.package.as_str())
}
pub fn rule_source(&self) -> &'static str {
COMPARE_SOURCE
}
}
#[derive(Debug)]
pub enum CompareStepError {
Plan(PlanError),
Execute(ExecuteError),
}
impl From<PlanError> for CompareStepError {
fn from(error: PlanError) -> Self {
Self::Plan(error)
}
}
impl From<ExecuteError> for CompareStepError {
fn from(error: ExecuteError) -> Self {
Self::Execute(error)
}
}
impl fmt::Display for CompareStepError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Plan(error) => error.fmt(formatter),
Self::Execute(error) => error.fmt(formatter),
}
}
}
impl std::error::Error for CompareStepError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Plan(error) => Some(error),
Self::Execute(error) => Some(error),
}
}
}
pub fn compare_step(
root: &Path,
workspace: &Workspace,
) -> Result<Vec<CompareDifference>, CompareStepError> {
let plans = plan(workspace, None)?;
compare_plans(root, workspace, &plans)
}
pub fn compare_plans(
root: &Path,
workspace: &Workspace,
plans: &[GenerationPlan],
) -> Result<Vec<CompareDifference>, CompareStepError> {
let mut differences = Vec::new();
for plan in plans {
let package_root = plan.package_root().map_or("", RelativePath::as_str);
let package_dir = guarded(root, package_root, false)?;
guarded(&package_dir, plan.crate_root().as_str(), true)?;
let (package_dir, tree) = generate_tree(root, plan)?;
let generated = generated_paths(&tree);
for (path, file) in generated.iter().zip(tree.files()) {
match checked_in(&package_dir, path) {
CheckedIn::Missing => differences.push(difference(
plan.package_id(),
path.clone(),
DifferenceKind::Missing,
)),
CheckedIn::Unreadable => differences.push(difference(
plan.package_id(),
path.clone(),
DifferenceKind::Differing,
)),
CheckedIn::Bytes(bytes) if bytes != file.bytes() => differences.push(difference(
plan.package_id(),
path.clone(),
DifferenceKind::Differing,
)),
CheckedIn::Bytes(_) => {}
}
}
for classification in workspace.classifications().iter().filter(|classification| {
classification.package() == plan.package_id()
&& classification.derived_output() == Some(plan.derived_output_id())
}) {
let Some(path) = package_relative(plan, classification.path()) else {
continue;
};
if !generated.contains(&path) {
differences.push(difference(plan.package_id(), path, DifferenceKind::Stale));
}
}
}
differences.sort_by(|left, right| {
left.package
.cmp(&right.package)
.then_with(|| left.path.cmp(&right.path))
});
Ok(differences)
}
fn package_relative(plan: &GenerationPlan, path: &RelativePath) -> Option<RelativePath> {
let Some(root) = plan.package_root() else {
return Some(path.clone());
};
let relative = path
.as_str()
.strip_prefix(root.as_str())?
.strip_prefix('/')?;
RelativePath::new(relative.to_owned()).ok()
}
pub fn composition_step(
root: &Path,
workspace: &Workspace,
plans: &[GenerationPlan],
) -> Result<Completion, ExecuteError> {
let schemas = plans
.iter()
.map(|plan| {
read_optional_file(root, plan.schema_path()).map(|bytes| {
SelectedSchema::new(plan.package_id().clone(), plan.schema_path().clone(), bytes)
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(match workspace.check_compositions(&schemas) {
Ok(()) => Completion::Passed,
Err(findings) => Completion::Failed(findings),
})
}
fn generated_paths(tree: &GeneratedTree) -> Vec<RelativePath> {
tree.files()
.iter()
.map(|file| {
RelativePath::new(file.path().to_owned())
.expect("generator outputs are valid relative paths")
})
.collect()
}
fn difference(package: &BoxId, path: RelativePath, kind: DifferenceKind) -> CompareDifference {
CompareDifference {
package: package.clone(),
path,
kind,
}
}
enum CheckedIn {
Missing,
Unreadable,
Bytes(Vec<u8>),
}
fn checked_in(package_dir: &Path, path: &RelativePath) -> CheckedIn {
let location = package_dir.join(path.as_str());
match fs::symlink_metadata(&location) {
Err(error) if error.kind() == io::ErrorKind::NotFound => CheckedIn::Missing,
Err(_) => CheckedIn::Unreadable,
Ok(_) => match guarded(package_dir, path.as_str(), true)
.ok()
.and_then(|path| fs::read(path).ok())
{
Some(bytes) => CheckedIn::Bytes(bytes),
None => CheckedIn::Unreadable,
},
}
}