use super::normalization::{format_rust_content, normalize_content, normalize_whitespace};
use crate::core::backend::GeneratedFile;
use crate::core::config::Language;
use crate::core::hash;
use rayon::prelude::*;
use std::path::Path;
pub fn diff_files(files: &[(Language, Vec<GeneratedFile>)], base_dir: &Path) -> anyhow::Result<Vec<String>> {
let all_items: Vec<_> = files
.iter()
.flat_map(|(lang, lang_files)| lang_files.iter().map(move |f| (*lang, f)))
.collect();
let diffs: Vec<String> = all_items
.par_iter()
.filter_map(|(lang, file)| {
let full_path = base_dir.join(&file.path);
if super::binary::is_base64_binary_output(&file.path) {
let matches = super::binary::decode_base64_binary(&file.path, &file.content)
.ok()
.zip(std::fs::read(&full_path).ok())
.is_some_and(|(generated, on_disk)| generated == on_disk);
return (!matches).then(|| format!("[{lang}] {}", file.path.display()));
}
let existing = std::fs::read_to_string(&full_path).unwrap_or_default();
let is_rust = file.path.extension().is_some_and(|ext| ext == "rs");
let generated_content = if file.generated_header
&& file.path.extension().is_some_and(|extension| extension == "toml")
&& !existing.is_empty()
{
super::scaffold::merge_managed_toml_preview(&existing, &file.content, base_dir, &file.path)
.unwrap_or_else(|_| file.content.clone())
} else {
file.content.clone()
};
let generated = normalize_content(&file.path, &generated_content);
let generated = if file.generated_header {
super::write::ensure_generated_header(&file.path, &generated)
} else {
generated
};
let on_disk = if is_rust {
format_rust_content(&full_path, &existing)
} else {
existing
};
let on_disk_body = hash::strip_hash_line(&on_disk);
if normalize_whitespace(&on_disk_body) != normalize_whitespace(&generated) {
Some(format!("[{lang}] {}", file.path.display()))
} else {
None
}
})
.collect();
Ok(diffs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::backend::GeneratedFile;
use base64::Engine;
const WRAPPER_BYTES: &[u8] = &[0x50, 0x4b, 0x03, 0x04, 0x00, 0xff, 0xfe, 0x01];
const JAR_PATH: &str = "packages/kotlin-android/gradle/wrapper/gradle-wrapper.jar";
fn jar_output(content: String) -> Vec<(Language, Vec<GeneratedFile>)> {
vec![(
Language::KotlinAndroid,
vec![GeneratedFile {
path: std::path::PathBuf::from(JAR_PATH),
content,
generated_header: false,
}],
)]
}
fn seed_jar(base: &Path, bytes: &[u8]) {
let full = base.join(JAR_PATH);
std::fs::create_dir_all(full.parent().expect("parent")).expect("mkdir");
std::fs::write(&full, bytes).expect("seed jar");
}
#[test]
fn a_binary_output_matching_its_generated_bytes_reports_no_diff() {
let dir = tempfile::tempdir().expect("tempdir");
let encoded = base64::engine::general_purpose::STANDARD.encode(WRAPPER_BYTES);
seed_jar(dir.path(), WRAPPER_BYTES);
let diffs = diff_files(&jar_output(encoded), dir.path()).expect("diff");
assert_eq!(diffs, Vec::<String>::new(), "an unchanged jar must not be reported");
}
#[test]
fn a_binary_output_differing_from_its_generated_bytes_is_reported() {
let dir = tempfile::tempdir().expect("tempdir");
let encoded = base64::engine::general_purpose::STANDARD.encode(WRAPPER_BYTES);
seed_jar(dir.path(), &[0x50, 0x4b, 0x03, 0x04]);
let diffs = diff_files(&jar_output(encoded), dir.path()).expect("diff");
assert_eq!(
diffs,
vec![format!("[kotlin_android] {JAR_PATH}")],
"a jar whose bytes differ from alef's is a real pending change"
);
}
}