const HASH_PREFIX: &str = "alef:hash:";
const DEFAULT_REGENERATE_COMMAND: &str = "alef generate";
const DEFAULT_VERIFY_COMMAND: &str = "alef verify --exit-code";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommentStyle {
DoubleSlash,
Hash,
Block,
}
pub fn header(style: CommentStyle) -> String {
render_header(style, &default_header_body())
}
pub fn header_for_config(style: CommentStyle, config: &crate::core::config::ResolvedCrateConfig) -> String {
let header_config = config.scaffold.as_ref().and_then(|s| s.generated_header.as_ref());
let body = match header_config {
Some(header) => {
let regenerate = header
.regenerate_command
.as_deref()
.unwrap_or(DEFAULT_REGENERATE_COMMAND);
let verify = header.verify_command.as_deref().unwrap_or(DEFAULT_VERIFY_COMMAND);
let issues_url = header.issues_url.as_deref().or_else(|| configured_header_url(config));
header_body(regenerate, verify, issues_url)
}
None => header_body(
DEFAULT_REGENERATE_COMMAND,
DEFAULT_VERIFY_COMMAND,
configured_header_url(config),
),
};
render_header(style, &body)
}
fn header_body(regenerate: &str, verify: &str, issues_url: Option<&str>) -> String {
let mut body = format!(
"This file is auto-generated by alef — DO NOT EDIT.\n\
To regenerate: {regenerate}\n\
To verify freshness: {verify}"
);
if let Some(url) = issues_url {
body.push_str(&format!("\nIssues & docs: {url}"));
}
body
}
fn configured_header_url(config: &crate::core::config::ResolvedCrateConfig) -> Option<&str> {
config
.package_metadata
.as_ref()
.and_then(|m| m.issues.as_deref().or(m.documentation.as_deref()))
}
fn default_header_body() -> String {
header_body(DEFAULT_REGENERATE_COMMAND, DEFAULT_VERIFY_COMMAND, None)
}
fn render_header(style: CommentStyle, body: &str) -> String {
match style {
CommentStyle::DoubleSlash => body.lines().map(|l| format!("// {l}\n")).collect(),
CommentStyle::Hash => body.lines().map(|l| format!("# {l}\n")).collect(),
CommentStyle::Block => {
let mut out = String::from("/*\n");
for line in body.lines() {
out.push_str(&format!(" * {line}\n"));
}
out.push_str(" */\n");
out
}
}
}
const HEADER_MARKER: &str = "auto-generated by alef";
const ALT_HEADER_MARKER: &str = "Generated by alef";
pub fn hash_content(content: &str) -> String {
blake3::hash(content.as_bytes()).to_hex().to_string()
}
pub fn compute_sources_hash(sources: &[std::path::PathBuf]) -> std::io::Result<String> {
let mut hasher = blake3::Hasher::new();
let mut sorted: Vec<&std::path::PathBuf> = sources.iter().collect();
sorted.sort();
for source in sorted {
let content = std::fs::read(source)?;
hasher.update(b"src\0");
hasher.update(source.to_string_lossy().as_bytes());
hasher.update(b"\0");
hasher.update(&content);
}
Ok(hasher.finalize().to_hex().to_string())
}
pub fn compute_crate_sources_hash(
crate_cfg: &crate::core::config::resolved::ResolvedCrateConfig,
) -> std::io::Result<String> {
let mut all_sources: Vec<&std::path::PathBuf> = Vec::new();
for src in &crate_cfg.sources {
all_sources.push(src);
}
for sc in &crate_cfg.source_crates {
for src in &sc.sources {
all_sources.push(src);
}
}
all_sources.sort();
all_sources.dedup();
let mut hasher = blake3::Hasher::new();
for source in all_sources {
let content = std::fs::read(source)?;
hasher.update(b"src\0");
hasher.update(source.to_string_lossy().as_bytes());
hasher.update(b"\0");
hasher.update(&content);
}
Ok(hasher.finalize().to_hex().to_string())
}
pub fn compute_inputs_hash(sources_hash: &str, alef_toml_bytes: &[u8]) -> String {
let alef_rev = crate::core::template_versions::precommit::ALEF_REV;
let mut hasher = blake3::Hasher::new();
hasher.update(b"alef:inputs\0");
hasher.update(alef_rev.as_bytes());
hasher.update(b"\0");
hasher.update(sources_hash.as_bytes());
hasher.update(b"\0");
hasher.update(alef_toml_bytes);
hasher.finalize().to_hex().to_string()
}
#[doc(hidden)]
pub fn compute_file_hash(sources_hash: &str, content: &str) -> String {
let stripped = strip_hash_line(content);
let mut hasher = blake3::Hasher::new();
hasher.update(b"sources\0");
hasher.update(sources_hash.as_bytes());
hasher.update(b"\0content\0");
hasher.update(stripped.as_bytes());
hasher.finalize().to_hex().to_string()
}
pub fn inject_hash_line(content: &str, hash: &str) -> String {
let mut result = String::with_capacity(content.len() + 80);
let mut injected = false;
for (i, line) in content.lines().enumerate() {
result.push_str(line);
result.push('\n');
if !injected && i < 10 && (line.contains(HEADER_MARKER) || line.contains(ALT_HEADER_MARKER)) {
let trimmed = line.trim();
let hash_line = if trimmed.starts_with("<!--") {
format!("<!-- {HASH_PREFIX}{hash} -->")
} else if trimmed.starts_with("//") {
format!("// {HASH_PREFIX}{hash}")
} else if trimmed.starts_with('#') {
format!("# {HASH_PREFIX}{hash}")
} else if trimmed.starts_with("/*") || trimmed.starts_with('*') || trimmed.ends_with("*/") {
format!(" * {HASH_PREFIX}{hash}")
} else {
format!("// {HASH_PREFIX}{hash}")
};
result.push_str(&hash_line);
result.push('\n');
injected = true;
}
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
pub fn extract_hash(content: &str) -> Option<String> {
for (i, line) in content.lines().enumerate() {
if i >= 10 {
break;
}
if let Some(pos) = line.find(HASH_PREFIX) {
let rest = &line[pos + HASH_PREFIX.len()..];
let hex = rest.trim().trim_end_matches("*/").trim_end_matches("-->").trim();
if !hex.is_empty() {
return Some(hex.to_string());
}
}
}
None
}
pub fn strip_hash_line(content: &str) -> String {
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if line.contains(HASH_PREFIX) {
continue;
}
result.push_str(line);
result.push('\n');
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
#[cfg(test)]
mod tests;