use super::error::{ProfileError, ProfileResult};
use super::fsops::calculate_integrity;
use super::lockfile::{ProfileLockEntry, ProfileLockfile};
use std::path::Path;
pub fn verify_profile(workspace: &Path, profile_name: &str, verbose: bool) -> ProfileResult<()> {
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
let lockfile = ProfileLockfile::load(&lockfile_path)?;
let entry = lockfile
.get_profile(profile_name)
.ok_or_else(|| ProfileError::NotInstalled {
name: profile_name.to_string(),
})?;
verify_profile_entry(workspace, entry, verbose)
}
pub fn verify_all_profiles(workspace: &Path, verbose: bool) -> ProfileResult<()> {
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
let lockfile = ProfileLockfile::load(&lockfile_path)?;
if lockfile.profiles.is_empty() {
if verbose {
println!("No profiles installed");
}
return Ok(());
}
for entry in lockfile.profiles.values() {
verify_profile_entry(workspace, entry, verbose)?;
}
println!("All profiles verified successfully");
Ok(())
}
fn verify_profile_entry(
workspace: &Path,
entry: &ProfileLockEntry,
verbose: bool,
) -> ProfileResult<()> {
if verbose {
println!("Verifying profile '{}'...", entry.name);
println!(" Stored integrity: {}", entry.integrity);
}
if entry.integrity.is_empty() {
if verbose {
println!(" WARNING: No integrity hash stored (legacy lockfile)");
println!(" Skipping verification - reinstall profile to add integrity checking");
}
return Ok(());
}
let absolute_files: Vec<String> = entry
.files
.iter()
.map(|rel| workspace.join(rel).to_string_lossy().to_string())
.collect();
let actual = calculate_integrity(&absolute_files)?;
if actual != entry.integrity {
return Err(ProfileError::IntegrityCheckFailed {
profile: entry.name.clone(),
expected: entry.integrity.clone(),
actual,
});
}
if verbose {
println!(" Calculated integrity: {actual}");
println!(" Integrity OK ({} files)", entry.files.len());
} else {
println!(
"Profile '{}' verified ({} files)",
entry.name,
entry.files.len()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profiles::lockfile::ProfileLockEntry;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_verify_succeeds_with_matching_integrity() {
let temp = tempdir().unwrap();
let workspace = temp.path();
fs::write(workspace.join("test1.txt"), "content 1").unwrap();
fs::write(workspace.join("test2.txt"), "content 2").unwrap();
let files = vec![
workspace.join("test1.txt").to_string_lossy().to_string(),
workspace.join("test2.txt").to_string_lossy().to_string(),
];
let integrity = calculate_integrity(&files).unwrap();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
fs::create_dir_all(lockfile_path.parent().unwrap()).unwrap();
let entry = ProfileLockEntry {
name: "test".to_string(),
version: "1.0.0".to_string(),
installed_at: "2025-01-11".to_string(),
files: vec!["test1.txt".to_string(), "test2.txt".to_string()],
integrity,
commit: None,
provider_id: None,
source: None,
};
let mut lockfile = ProfileLockfile::new();
lockfile.add_profile(entry);
lockfile.save(&lockfile_path).unwrap();
let result = verify_profile(workspace, "test", false);
assert!(result.is_ok());
}
#[test]
fn test_verify_fails_with_modified_files() {
let temp = tempdir().unwrap();
let workspace = temp.path();
fs::write(workspace.join("test1.txt"), "content 1").unwrap();
fs::write(workspace.join("test2.txt"), "content 2").unwrap();
let files = vec![
workspace.join("test1.txt").to_string_lossy().to_string(),
workspace.join("test2.txt").to_string_lossy().to_string(),
];
let integrity = calculate_integrity(&files).unwrap();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
fs::create_dir_all(lockfile_path.parent().unwrap()).unwrap();
let entry = ProfileLockEntry {
name: "test".to_string(),
version: "1.0.0".to_string(),
installed_at: "2025-01-11".to_string(),
files: vec!["test1.txt".to_string(), "test2.txt".to_string()],
integrity,
commit: None,
provider_id: None,
source: None,
};
let mut lockfile = ProfileLockfile::new();
lockfile.add_profile(entry);
lockfile.save(&lockfile_path).unwrap();
fs::write(workspace.join("test1.txt"), "MODIFIED").unwrap();
let result = verify_profile(workspace, "test", false);
assert!(result.is_err());
match result {
Err(ProfileError::IntegrityCheckFailed { profile, .. }) => {
assert_eq!(profile, "test");
}
_ => panic!("Expected IntegrityCheckFailed error"),
}
}
#[test]
fn test_verify_fails_with_missing_files() {
let temp = tempdir().unwrap();
let workspace = temp.path();
fs::write(workspace.join("test1.txt"), "content 1").unwrap();
fs::write(workspace.join("test2.txt"), "content 2").unwrap();
let files = vec![
workspace.join("test1.txt").to_string_lossy().to_string(),
workspace.join("test2.txt").to_string_lossy().to_string(),
];
let integrity = calculate_integrity(&files).unwrap();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
fs::create_dir_all(lockfile_path.parent().unwrap()).unwrap();
let entry = ProfileLockEntry {
name: "test".to_string(),
version: "1.0.0".to_string(),
installed_at: "2025-01-11".to_string(),
files: vec!["test1.txt".to_string(), "test2.txt".to_string()],
integrity,
commit: None,
provider_id: None,
source: None,
};
let mut lockfile = ProfileLockfile::new();
lockfile.add_profile(entry);
lockfile.save(&lockfile_path).unwrap();
fs::remove_file(workspace.join("test2.txt")).unwrap();
let result = verify_profile(workspace, "test", false);
assert!(result.is_err());
match result {
Err(ProfileError::IntegrityCheckFailed { .. }) => {}
_ => panic!("Expected IntegrityCheckFailed error"),
}
}
#[test]
fn test_verify_all_profiles() {
let temp = tempdir().unwrap();
let workspace = temp.path();
fs::write(workspace.join("profile1.txt"), "content 1").unwrap();
fs::write(workspace.join("profile2.txt"), "content 2").unwrap();
let files1 = vec![workspace.join("profile1.txt").to_string_lossy().to_string()];
let integrity1 = calculate_integrity(&files1).unwrap();
let files2 = vec![workspace.join("profile2.txt").to_string_lossy().to_string()];
let integrity2 = calculate_integrity(&files2).unwrap();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
fs::create_dir_all(lockfile_path.parent().unwrap()).unwrap();
let mut lockfile = ProfileLockfile::new();
lockfile.add_profile(ProfileLockEntry {
name: "profile1".to_string(),
version: "1.0.0".to_string(),
installed_at: "2025-01-11".to_string(),
files: vec!["profile1.txt".to_string()],
integrity: integrity1,
commit: None,
provider_id: None,
source: None,
});
lockfile.add_profile(ProfileLockEntry {
name: "profile2".to_string(),
version: "1.0.0".to_string(),
installed_at: "2025-01-11".to_string(),
files: vec!["profile2.txt".to_string()],
integrity: integrity2,
commit: None,
provider_id: None,
source: None,
});
lockfile.save(&lockfile_path).unwrap();
let result = verify_all_profiles(workspace, false);
assert!(result.is_ok());
}
#[test]
fn test_verify_skips_legacy_without_integrity() {
let temp = tempdir().unwrap();
let workspace = temp.path();
fs::write(workspace.join("test.txt"), "content").unwrap();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
fs::create_dir_all(lockfile_path.parent().unwrap()).unwrap();
let entry = ProfileLockEntry {
name: "legacy".to_string(),
version: "1.0.0".to_string(),
installed_at: "2025-01-11".to_string(),
files: vec!["test.txt".to_string()],
integrity: String::new(), commit: None,
provider_id: None,
source: None,
};
let mut lockfile = ProfileLockfile::new();
lockfile.add_profile(entry);
lockfile.save(&lockfile_path).unwrap();
let result = verify_profile(workspace, "legacy", false);
assert!(result.is_ok());
}
#[test]
fn test_verify_not_installed() {
let temp = tempdir().unwrap();
let workspace = temp.path();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
fs::create_dir_all(lockfile_path.parent().unwrap()).unwrap();
let lockfile = ProfileLockfile::new();
lockfile.save(&lockfile_path).unwrap();
let result = verify_profile(workspace, "nonexistent", false);
assert!(result.is_err());
match result {
Err(ProfileError::NotInstalled { name }) => {
assert_eq!(name, "nonexistent");
}
_ => panic!("Expected NotInstalled error"),
}
}
}