use crate::error::{JsGenError, Result};
use crate::hir::GenerationMetadata;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileManifest {
pub manifest_version: u32,
pub output_path: String,
pub output_hash: String,
pub generation: GenerationMetadata,
}
impl FileManifest {
pub const VERSION: u32 = 1;
#[must_use]
pub fn new(
output_path: impl Into<String>,
output_hash: impl Into<String>,
generation: GenerationMetadata,
) -> Self {
Self {
manifest_version: Self::VERSION,
output_path: output_path.into(),
output_hash: output_hash.into(),
generation,
}
}
#[must_use]
pub fn manifest_path(generated_path: &Path) -> std::path::PathBuf {
let mut path = generated_path.to_path_buf();
let mut filename = generated_path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
filename.push_str(".manifest.json");
path.set_file_name(filename);
path
}
pub fn write(&self, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn read(path: &Path) -> Result<Self> {
let json = std::fs::read_to_string(path)?;
let manifest: Self = serde_json::from_str(&json)?;
Ok(manifest)
}
}
#[must_use]
pub fn hash_file_contents(contents: &str) -> String {
blake3::hash(contents.as_bytes()).to_hex().to_string()
}
pub fn verify(generated_path: &Path) -> Result<()> {
let manifest_path = FileManifest::manifest_path(generated_path);
let manifest = FileManifest::read(&manifest_path).map_err(|_| JsGenError::ManifestError {
path: generated_path.display().to_string(),
reason: format!("manifest not found at {}", manifest_path.display()),
})?;
let contents = std::fs::read_to_string(generated_path)?;
let actual_hash = hash_file_contents(&contents);
if actual_hash != manifest.output_hash {
return Err(JsGenError::HashMismatch {
path: generated_path.display().to_string(),
expected: manifest.output_hash,
actual: actual_hash,
});
}
Ok(())
}
pub fn write_with_manifest(
path: &Path,
contents: &str,
metadata: GenerationMetadata,
) -> Result<()> {
let hash = hash_file_contents(contents);
std::fs::write(path, contents)?;
let manifest = FileManifest::new(
path.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default(),
hash,
metadata,
);
let manifest_path = FileManifest::manifest_path(path);
manifest.write(&manifest_path)?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use tempfile::TempDir;
fn test_metadata() -> GenerationMetadata {
GenerationMetadata {
tool: "probar-js-gen".to_string(),
version: "0.1.0".to_string(),
input_hash: "input123".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
regenerate_cmd: "probar gen js".to_string(),
}
}
#[test]
fn manifest_path_generation() {
let path = Path::new("/foo/bar/worker.js");
let manifest = FileManifest::manifest_path(path);
assert_eq!(manifest.file_name().unwrap(), "worker.js.manifest.json");
}
#[test]
fn hash_deterministic() {
let content = "const x = 42;";
let hash1 = hash_file_contents(content);
let hash2 = hash_file_contents(content);
assert_eq!(hash1, hash2);
}
#[test]
fn hash_changes_with_content() {
let hash1 = hash_file_contents("const x = 42;");
let hash2 = hash_file_contents("const x = 43;");
assert_ne!(hash1, hash2);
}
#[test]
fn write_and_verify_success() -> Result<()> {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.js");
let contents = "const x = 42;";
write_with_manifest(&path, contents, test_metadata())?;
verify(&path)?;
Ok(())
}
#[test]
fn verify_detects_modification() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.js");
let contents = "const x = 42;";
write_with_manifest(&path, contents, test_metadata()).unwrap();
std::fs::write(&path, "const x = 999;").unwrap();
let result = verify(&path);
assert!(result.is_err());
match result.unwrap_err() {
JsGenError::HashMismatch {
path: _,
expected,
actual,
} => {
assert_ne!(expected, actual);
}
e => panic!("Expected HashMismatch, got {:?}", e),
}
}
#[test]
fn verify_missing_manifest() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.js");
std::fs::write(&path, "const x = 42;").unwrap();
let result = verify(&path);
assert!(result.is_err());
match result.unwrap_err() {
JsGenError::ManifestError { .. } => {}
e => panic!("Expected ManifestError, got {:?}", e),
}
}
#[test]
fn manifest_serialization() {
let manifest = FileManifest::new("test.js", "abc123", test_metadata());
let json = serde_json::to_string(&manifest).unwrap();
let parsed: FileManifest = serde_json::from_str(&json).unwrap();
assert_eq!(manifest.output_path, parsed.output_path);
assert_eq!(manifest.output_hash, parsed.output_hash);
assert_eq!(manifest.manifest_version, FileManifest::VERSION);
}
}