use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use crate::emit::GENERATED_MARKER;
use crate::emit::HEADER;
use crate::error::Error;
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedFile {
path: PathBuf,
source: String,
}
impl GeneratedFile {
pub fn new(path: impl Into<PathBuf>, source: impl Into<String>) -> Self {
return Self {
path: path.into(),
source: source.into(),
};
}
pub fn path(&self) -> &Path {
return &self.path;
}
pub fn source(&self) -> &str {
return &self.source;
}
fn body(&self) -> &str {
return self.source.strip_prefix(HEADER).unwrap_or(&self.source);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedPackage {
root: String,
children: Vec<GeneratedFile>,
}
impl GeneratedPackage {
pub fn new(root: impl Into<String>, children: Vec<GeneratedFile>) -> Self {
return Self {
root: root.into(),
children,
};
}
pub fn root_source(&self) -> &str {
return &self.root;
}
pub fn children(&self) -> &[GeneratedFile] {
return &self.children;
}
pub fn file_count(&self) -> usize {
return self.children.len().saturating_add(1);
}
pub fn combined_source(&self) -> String {
let mut combined = String::from(HEADER);
combined.push_str(self.root.strip_prefix(HEADER).unwrap_or(&self.root));
for child in &self.children {
combined.push_str(child.body());
}
return combined;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageDrift {
None,
Absent(PathBuf),
Differs(PathBuf),
Stale(PathBuf),
}
fn companion_directory(output_path: &Path) -> Option<PathBuf> {
let stem = output_path.file_stem()?;
let parent = output_path.parent().unwrap_or_else(|| return Path::new(""));
return Some(parent.join(stem));
}
pub fn write_package(output_path: &Path, package: &GeneratedPackage) -> Result<()> {
let stale = audit(output_path, package)?;
crate::write_output(output_path, package.root_source())?;
let parent = output_path.parent().unwrap_or_else(|| return Path::new(""));
for child in package.children() {
crate::write_output(&parent.join(child.path()), child.source())?;
}
for path in stale {
std::fs::remove_file(&path).map_err(|source| {
return Error::WriteOutput {
path: path.display().to_string(),
source,
};
})?;
}
if let Some(directory) = companion_directory(output_path) {
remove_empty_directories(&directory);
}
return Ok(());
}
pub fn check_package(output_path: &Path, package: &GeneratedPackage) -> Result<PackageDrift> {
let stale = audit(output_path, package)?;
if let Some(drift) = compare(output_path, package.root_source())? {
return Ok(drift);
}
let parent = output_path.parent().unwrap_or_else(|| return Path::new(""));
for child in package.children() {
if let Some(drift) = compare(&parent.join(child.path()), child.source())? {
return Ok(drift);
}
}
if let Some(path) = stale.into_iter().next() {
return Ok(PackageDrift::Stale(path));
}
return Ok(PackageDrift::None);
}
fn compare(path: &Path, source: &str) -> Result<Option<PackageDrift>> {
return match crate::check_output(path, source)? {
crate::Drift::None => Ok(None),
crate::Drift::Absent => Ok(Some(PackageDrift::Absent(path.to_path_buf()))),
crate::Drift::Differs => Ok(Some(PackageDrift::Differs(path.to_path_buf()))),
};
}
pub(crate) fn companion_of(output_path: &Path) -> Result<PathBuf> {
return companion_directory(output_path)
.filter(|directory| return directory != output_path)
.ok_or_else(|| {
return Error::UnsplittableOutput {
path: output_path.display().to_string(),
};
});
}
fn audit(output_path: &Path, package: &GeneratedPackage) -> Result<Vec<PathBuf>> {
let directory = match companion_of(output_path) {
Ok(directory) => directory,
Err(_) if package.children().is_empty() => return Ok(Vec::new()),
Err(error) => return Err(error),
};
let parent = output_path.parent().unwrap_or_else(|| return Path::new(""));
for child in package.children() {
let path = child.path();
let contained = path
.components()
.all(|component| return matches!(component, Component::Normal(_)))
&& parent.join(path).starts_with(&directory);
if !contained {
return Err(Error::OutsideOutput {
path: path.display().to_string(),
directory: directory.display().to_string(),
});
}
}
match std::fs::symlink_metadata(&directory) {
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => {
return Err(Error::UnownedOutput {
path: directory.display().to_string(),
});
}
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(source) => {
return Err(Error::ReadOutput {
path: directory.display().to_string(),
source,
});
}
}
let expected: Vec<PathBuf> = package
.children()
.iter()
.map(|child| return parent.join(child.path()))
.collect();
let mut stale = Vec::new();
collect_stale(&directory, &expected, &mut stale)?;
stale.sort();
return Ok(stale);
}
fn collect_stale(directory: &Path, expected: &[PathBuf], stale: &mut Vec<PathBuf>) -> Result<()> {
let entries = std::fs::read_dir(directory).map_err(|source| {
return Error::ReadOutput {
path: directory.display().to_string(),
source,
};
})?;
for entry in entries {
let entry = entry.map_err(|source| {
return Error::ReadOutput {
path: directory.display().to_string(),
source,
};
})?;
let path = entry.path();
let kind = entry.file_type().map_err(|source| {
return Error::ReadOutput {
path: path.display().to_string(),
source,
};
})?;
if kind.is_symlink() {
return Err(Error::UnownedOutput {
path: path.display().to_string(),
});
}
if kind.is_dir() {
collect_stale(&path, expected, stale)?;
continue;
}
if !kind.is_file() || !is_generated(&path)? {
return Err(Error::UnownedOutput {
path: path.display().to_string(),
});
}
if expected.iter().any(|candidate| return *candidate == path) {
continue;
}
stale.push(path);
}
return Ok(());
}
fn is_generated(path: &Path) -> Result<bool> {
let read = |source| {
return Error::ReadOutput {
path: path.display().to_string(),
source,
};
};
let marker = GENERATED_MARKER.as_bytes();
let mut file = std::fs::File::open(path).map_err(read)?;
let mut opening = vec![0_u8; marker.len()];
return match std::io::Read::read_exact(&mut file, &mut opening) {
Ok(()) => Ok(opening == marker),
Err(source) if source.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
Err(source) => Err(read(source)),
};
}
fn remove_empty_directories(directory: &Path) {
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
for entry in entries.flatten() {
if entry.file_type().map(|kind| return kind.is_dir()).unwrap_or(false) {
remove_empty_directories(&entry.path());
}
}
let _ = std::fs::remove_dir(directory);
}