use crate::context::frame::{id, Frame};
use crate::error::StorageError;
use crate::types::FrameID;
use bincode;
use std::fs;
use std::path::{Path, PathBuf};
pub struct FrameStorage {
root: PathBuf,
}
impl FrameStorage {
pub fn new<P: AsRef<Path>>(root: P) -> Result<Self, StorageError> {
let root = root.as_ref().to_path_buf();
let frames_dir = root.join("frames");
fs::create_dir_all(&frames_dir).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to create frames directory at {:?}: {}",
frames_dir, e
),
))
})?;
Ok(Self { root })
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn store(&self, frame: &Frame) -> Result<(), StorageError> {
let agent_id = if frame.agent_id.is_empty() {
return Err(StorageError::InvalidPath(
"Frame missing structural agent_id".to_string(),
));
} else {
frame.agent_id.as_str()
};
let computed_id =
id::compute_frame_id(&frame.basis, &frame.content, &frame.frame_type, agent_id)?;
if computed_id != frame.frame_id {
return Err(StorageError::HashMismatch {
expected: frame.frame_id,
actual: computed_id,
});
}
if self.exists(&frame.frame_id)? {
return Ok(()); }
let frame_path = self.frame_path(&frame.frame_id);
let temp_path = frame_path.with_extension("frame.tmp");
if let Some(parent) = frame_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create parent directory {:?}: {}", parent, e),
))
})?;
}
let serialized = bincode::serialize(frame).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to serialize frame: {}", e),
))
})?;
fs::write(&temp_path, &serialized).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write frame to {:?}: {}", temp_path, e),
))
})?;
fs::rename(&temp_path, &frame_path).map_err(|e| {
let _ = fs::remove_file(&temp_path);
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to rename temp file to {:?}: {}", frame_path, e),
))
})?;
Ok(())
}
pub fn get(&self, frame_id: &FrameID) -> Result<Option<Frame>, StorageError> {
let frame_path = self.frame_path(frame_id);
if !frame_path.exists() {
return Ok(None);
}
let bytes = fs::read(&frame_path).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to read frame from {:?}: {}", frame_path, e),
))
})?;
let mut frame: Frame = bincode::deserialize(&bytes).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to deserialize frame from {:?}: {}", frame_path, e),
))
})?;
if frame.agent_id.is_empty() {
if let Some(agent_id) = frame.metadata.get("agent_id") {
frame.agent_id = agent_id.clone();
}
}
if frame.agent_id.is_empty() {
return Err(StorageError::InvalidPath(
"Frame missing structural agent_id".to_string(),
));
}
if frame.frame_id != *frame_id {
return Err(StorageError::HashMismatch {
expected: *frame_id,
actual: frame.frame_id,
});
}
let computed_id = id::compute_frame_id(
&frame.basis,
&frame.content,
&frame.frame_type,
&frame.agent_id,
)?;
if computed_id != frame.frame_id {
return Err(StorageError::HashMismatch {
expected: frame.frame_id,
actual: computed_id,
});
}
Ok(Some(frame))
}
pub fn exists(&self, frame_id: &FrameID) -> Result<bool, StorageError> {
let frame_path = self.frame_path(frame_id);
Ok(frame_path.exists())
}
pub fn purge(&self, frame_id: &FrameID) -> Result<(), StorageError> {
let frame_path = self.frame_path(frame_id);
if frame_path.exists() {
fs::remove_file(&frame_path).map_err(|e| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to purge frame {:?}: {}", frame_path, e),
))
})?;
}
Ok(())
}
fn frame_path(&self, frame_id: &FrameID) -> PathBuf {
let hex: String = frame_id.iter().map(|b| format!("{:02x}", b)).collect();
let prefix1 = &hex[0..2];
let prefix2 = &hex[2..4];
self.root
.join("frames")
.join(prefix1)
.join(prefix2)
.join(format!("{}.frame", hex))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::frame::{Basis, Frame};
use crate::types::NodeID;
use std::collections::HashMap;
use tempfile::TempDir;
#[test]
fn test_store_and_retrieve() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test frame content".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
storage.store(&frame).unwrap();
let retrieved = storage.get(&frame.frame_id).unwrap();
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.frame_id, frame.frame_id);
assert_eq!(retrieved.content, frame.content);
assert_eq!(retrieved.frame_type, frame.frame_type);
}
#[test]
fn test_deduplication() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test content".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
storage.store(&frame).unwrap();
storage.store(&frame).unwrap();
assert!(storage.exists(&frame.frame_id).unwrap());
let frame_path = storage.frame_path(&frame.frame_id);
assert!(frame_path.exists());
}
#[test]
fn test_get_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let frame_id: FrameID = [0u8; 32];
let result = storage.get(&frame_id).unwrap();
assert!(result.is_none());
}
#[test]
fn test_exists() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
assert!(!storage.exists(&frame.frame_id).unwrap());
storage.store(&frame).unwrap();
assert!(storage.exists(&frame.frame_id).unwrap());
}
#[test]
fn test_path_structure() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let frame_id: FrameID = [
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
];
let path = storage.frame_path(&frame_id);
assert!(path.to_string_lossy().contains("frames/12/34"));
assert!(path.to_string_lossy().ends_with(".frame"));
assert!(path
.to_string_lossy()
.contains("123456789abcdef0112233445566778899aabbccddeeff000102030405060708"));
}
#[test]
fn test_corruption_detection() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let mut frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
frame.frame_id[0] = 0xFF;
let result = storage.store(&frame);
assert!(result.is_err());
match result {
Err(StorageError::HashMismatch { .. }) => {}
_ => panic!("Expected HashMismatch error"),
}
}
#[test]
fn test_purge_removes_file() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
storage.store(&frame).unwrap();
assert!(storage.exists(&frame.frame_id).unwrap());
storage.purge(&frame.frame_id).unwrap();
assert!(!storage.exists(&frame.frame_id).unwrap());
}
#[test]
fn test_purge_nonexistent_idempotent() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let frame_id: FrameID = [0u8; 32];
storage.purge(&frame_id).unwrap();
}
#[test]
fn test_get_ignores_non_structural_metadata_mutation() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
storage.store(&frame).unwrap();
let frame_path = storage.frame_path(&frame.frame_id);
let bytes = fs::read(&frame_path).unwrap();
let mut stored_frame: Frame = bincode::deserialize(&bytes).unwrap();
stored_frame
.metadata
.insert("provider".to_string(), "mutated-provider".to_string());
let updated = bincode::serialize(&stored_frame).unwrap();
fs::write(&frame_path, updated).unwrap();
let loaded = storage.get(&frame.frame_id).unwrap().unwrap();
assert_eq!(loaded.frame_id, frame.frame_id);
assert_eq!(loaded.content, frame.content);
}
#[test]
fn test_get_detects_structural_content_corruption() {
let temp_dir = TempDir::new().unwrap();
let storage = FrameStorage::new(temp_dir.path()).unwrap();
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
let frame_type = "test".to_string();
let agent_id = "test-agent".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id, metadata).unwrap();
storage.store(&frame).unwrap();
let frame_path = storage.frame_path(&frame.frame_id);
let bytes = fs::read(&frame_path).unwrap();
let mut stored_frame: Frame = bincode::deserialize(&bytes).unwrap();
stored_frame.content = b"corrupted".to_vec();
let updated = bincode::serialize(&stored_frame).unwrap();
fs::write(&frame_path, updated).unwrap();
let result = storage.get(&frame.frame_id);
assert!(matches!(result, Err(StorageError::HashMismatch { .. })));
}
}