use crate::utils::error::{Error, Result};
use oxigraph::model::{Dataset, GraphName, NamedNode, Quad};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::SystemTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Statement {
pub subject: String,
pub predicate: String,
pub object: String,
pub graph: Option<String>,
}
impl From<oxigraph::model::Quad> for Statement {
fn from(quad: oxigraph::model::Quad) -> Self {
fn strip_brackets(s: &str) -> String {
if s.starts_with('<') && s.ends_with('>') {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
let graph = match &quad.graph_name {
oxigraph::model::GraphName::DefaultGraph => None,
oxigraph::model::GraphName::NamedNode(nn) => Some(strip_brackets(&nn.to_string())),
oxigraph::model::GraphName::BlankNode(bn) => Some(bn.to_string()),
};
Self {
subject: strip_brackets(&quad.subject.to_string()),
predicate: strip_brackets(&quad.predicate.to_string()),
object: strip_brackets(&quad.object.to_string()),
graph,
}
}
}
mod arc_vec_serde {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(
arc: &Arc<Vec<Statement>>, serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::Serialize;
(*arc).serialize(serializer)
}
pub fn deserialize<'de, D>(
deserializer: D,
) -> std::result::Result<Arc<Vec<Statement>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::Deserialize;
Vec::<Statement>::deserialize(deserializer).map(Arc::new)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, Default)]
pub struct SigmaSnapshotId(String);
impl SigmaSnapshotId {
pub fn from_digest(data: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
Self(format!("{:x}", result))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for SigmaSnapshotId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigmaSnapshot {
pub id: SigmaSnapshotId,
pub parent_id: Option<SigmaSnapshotId>,
#[serde(with = "arc_vec_serde")]
pub triples: Arc<Vec<Statement>>,
pub version: String,
pub timestamp: SystemTime,
pub signature: String,
pub metadata: SnapshotMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SnapshotMetadata {
pub backward_compatible: bool,
pub description: String,
pub sectors: Vec<String>,
pub tags: BTreeMap<String, String>,
}
impl SigmaSnapshot {
pub fn new(
parent_id: Option<SigmaSnapshotId>, triples: Vec<Statement>, version: String,
signature: String, metadata: SnapshotMetadata,
) -> Self {
let serialized = serde_json::to_vec(&triples).unwrap_or_default();
let id = SigmaSnapshotId::from_digest(&serialized);
Self {
id,
parent_id,
triples: Arc::new(triples),
version,
timestamp: SystemTime::now(),
signature,
metadata,
}
}
pub fn as_dataset(&self) -> Result<Dataset> {
let mut dataset = Dataset::default();
for stmt in self.triples.iter() {
let subject = NamedNode::new(&stmt.subject)
.map_err(|e| Error::new(&format!("Invalid subject IRI: {}", e)))?;
let predicate = NamedNode::new(&stmt.predicate)
.map_err(|e| Error::new(&format!("Invalid predicate IRI: {}", e)))?;
let object = NamedNode::new(&stmt.object)
.map_err(|e| Error::new(&format!("Invalid object IRI: {}", e)))?;
let graph_name = if let Some(graph) = &stmt.graph {
GraphName::NamedNode(
NamedNode::new(graph)
.map_err(|e| Error::new(&format!("Invalid graph IRI: {}", e)))?,
)
} else {
GraphName::DefaultGraph
};
let quad = Quad::new(subject, predicate, object, graph_name);
dataset.insert(&quad);
}
Ok(dataset)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigmaOverlay {
pub base_id: SigmaSnapshotId,
pub additions: Vec<Statement>,
pub removals: Vec<(Option<String>, Option<String>, Option<String>)>,
pub id: String,
pub description: String,
}
impl SigmaOverlay {
pub fn new(
base_id: SigmaSnapshotId, additions: Vec<Statement>,
removals: Vec<(Option<String>, Option<String>, Option<String>)>, description: String,
) -> Self {
let id = format!("{}_overlay_{}", base_id, uuid::Uuid::new_v4());
Self {
base_id,
additions,
removals,
id,
description,
}
}
pub fn apply_to(&self, base: &SigmaSnapshot) -> SigmaSnapshot {
let mut new_triples = (*base.triples).clone();
new_triples.extend(self.additions.clone());
new_triples.retain(|stmt| {
for (s, p, o) in &self.removals {
let subj_match = s.is_none() || Some(stmt.subject.to_string()) == *s;
let pred_match = p.is_none() || Some(stmt.predicate.to_string()) == *p;
let obj_match = o.is_none() || Some(stmt.object.to_string()) == *o;
if subj_match && pred_match && obj_match {
return false; }
}
true });
let serialized = serde_json::to_vec(&new_triples).unwrap_or_default();
let new_id = SigmaSnapshotId::from_digest(&serialized);
SigmaSnapshot {
id: new_id,
parent_id: Some(base.id.clone()),
triples: Arc::new(new_triples),
version: format!("{}_overlay", base.version),
timestamp: SystemTime::now(),
signature: format!("{}_unsigned", base.signature),
metadata: base.metadata.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigmaReceipt {
pub snapshot_id: SigmaSnapshotId,
pub parent_snapshot_id: Option<SigmaSnapshotId>,
pub delta_description: String,
pub result: ValidationResult,
pub invariants_preserved: bool,
pub invariants_checked: Vec<String>,
pub test_results: BTreeMap<String, TestResult>,
pub performance_metrics: PerformanceMetrics,
pub signature: String,
pub timestamp: SystemTime,
pub error_message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidationResult {
Valid,
Invalid,
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub duration_ms: u64,
pub error: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub memory_bytes: u64,
pub operator_latency_us: u64,
pub slos_met: bool,
pub custom: BTreeMap<String, f64>,
}
impl SigmaReceipt {
pub fn new(
snapshot_id: SigmaSnapshotId, parent_snapshot_id: Option<SigmaSnapshotId>,
delta_description: String,
) -> Self {
Self {
snapshot_id,
parent_snapshot_id,
delta_description,
result: ValidationResult::Pending,
invariants_preserved: false,
invariants_checked: Vec::new(),
test_results: BTreeMap::new(),
performance_metrics: PerformanceMetrics::default(),
signature: String::new(),
timestamp: SystemTime::now(),
error_message: None,
}
}
pub fn mark_valid(mut self) -> Self {
self.result = ValidationResult::Valid;
self.invariants_preserved = true;
self
}
pub fn mark_invalid(mut self, reason: String) -> Self {
self.result = ValidationResult::Invalid;
self.error_message = Some(reason);
self
}
pub fn sign(&mut self, signature: String) {
self.signature = signature;
}
}
pub struct SigmaRuntime {
snapshots: BTreeMap<SigmaSnapshotId, Arc<SigmaSnapshot>>,
current_snapshot: Arc<SigmaSnapshotId>,
#[allow(dead_code)] overlays: BTreeMap<String, SigmaOverlay>,
receipt_log: Vec<Arc<SigmaReceipt>>,
}
impl SigmaRuntime {
pub fn new(initial_snapshot: SigmaSnapshot) -> Self {
let snapshot_id = initial_snapshot.id.clone();
let mut snapshots = BTreeMap::new();
snapshots.insert(snapshot_id.clone(), Arc::new(initial_snapshot));
Self {
snapshots,
current_snapshot: Arc::new(snapshot_id),
overlays: BTreeMap::new(),
receipt_log: Vec::new(),
}
}
pub fn current_snapshot(&self) -> Option<Arc<SigmaSnapshot>> {
self.snapshots.get(self.current_snapshot.as_ref()).cloned()
}
pub fn get_snapshot(&self, id: &SigmaSnapshotId) -> Option<Arc<SigmaSnapshot>> {
self.snapshots.get(id).cloned()
}
pub fn store_snapshot(&mut self, snapshot: SigmaSnapshot) {
self.snapshots
.insert(snapshot.id.clone(), Arc::new(snapshot));
}
pub fn apply_overlay(
&mut self, base_id: &SigmaSnapshotId, overlay: SigmaOverlay,
) -> Result<SigmaSnapshot> {
let base = self
.get_snapshot(base_id)
.ok_or_else(|| Error::new("Base snapshot not found"))?;
let new_snapshot = overlay.apply_to(&base);
Ok(new_snapshot)
}
pub fn promote_snapshot(&mut self, id: &SigmaSnapshotId) -> Result<()> {
if !self.snapshots.contains_key(id) {
return Err(Error::new(&format!("Snapshot {} not found", id)));
}
self.current_snapshot = Arc::new(id.clone());
Ok(())
}
pub fn record_receipt(&mut self, receipt: SigmaReceipt) {
self.receipt_log.push(Arc::new(receipt));
}
pub fn get_receipts_for(&self, snapshot_id: &SigmaSnapshotId) -> Vec<Arc<SigmaReceipt>> {
self.receipt_log
.iter()
.filter(|r| r.snapshot_id == *snapshot_id)
.cloned()
.collect()
}
pub fn receipt_log(&self) -> &[Arc<SigmaReceipt>] {
&self.receipt_log
}
pub fn snapshot_count(&self) -> usize {
self.snapshots.len()
}
pub fn list_snapshots(&self) -> Vec<SigmaSnapshotId> {
self.snapshots.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_snapshot() -> SigmaSnapshot {
let metadata = SnapshotMetadata {
backward_compatible: true,
description: "Test snapshot".to_string(),
sectors: vec!["test".to_string()],
tags: BTreeMap::new(),
};
SigmaSnapshot::new(
None,
vec![],
"0.1.0".to_string(),
"test_signature".to_string(),
metadata,
)
}
#[test]
fn test_snapshot_creation() {
let snap = create_test_snapshot();
assert_eq!(snap.version, "0.1.0");
assert!(snap.parent_id.is_none());
assert!(!snap.id.0.is_empty());
}
#[test]
fn test_snapshot_id_determinism() {
let snap1 = create_test_snapshot();
let snap2 = create_test_snapshot();
assert_eq!(snap1.id, snap2.id);
}
#[test]
fn test_runtime_creation() {
let snap = create_test_snapshot();
let runtime = SigmaRuntime::new(snap.clone());
assert_eq!(runtime.snapshot_count(), 1);
assert!(runtime.current_snapshot().is_some());
assert_eq!(runtime.current_snapshot().unwrap().id, snap.id);
}
#[test]
fn test_snapshot_promotion() {
let snap1 = create_test_snapshot();
let snap1_id = snap1.id.clone();
let mut metadata = snap1.metadata.clone();
metadata.description = "Snapshot 2".to_string();
let snap2 = SigmaSnapshot::new(
Some(snap1_id.clone()),
vec![],
"0.2.0".to_string(),
"signature2".to_string(),
metadata,
);
let snap2_id = snap2.id.clone();
let mut runtime = SigmaRuntime::new(snap1.clone());
runtime.store_snapshot(snap2);
assert_eq!(runtime.current_snapshot().unwrap().id, snap1_id);
runtime.promote_snapshot(&snap2_id).unwrap();
assert_eq!(runtime.current_snapshot().unwrap().id, snap2_id);
}
#[test]
fn test_receipt_signing() {
let snap_id = SigmaSnapshotId::from_digest(b"test");
let mut receipt = SigmaReceipt::new(snap_id, None, "Test change".to_string());
receipt.sign("ml_dsa_signature_here".to_string());
assert_eq!(receipt.signature, "ml_dsa_signature_here");
}
#[test]
fn test_receipt_log() {
let snap = create_test_snapshot();
let mut runtime = SigmaRuntime::new(snap.clone());
let receipt = SigmaReceipt::new(snap.id.clone(), None, "Change".to_string());
let receipt = receipt.mark_valid();
runtime.record_receipt(receipt);
assert_eq!(runtime.receipt_log().len(), 1);
}
#[test]
fn test_as_dataset_empty() {
let snap = create_test_snapshot();
let dataset = snap.as_dataset().unwrap();
assert_eq!(dataset.len(), 0);
}
#[test]
fn test_as_dataset_with_triples() {
use oxigraph::model::{GraphName, NamedNode, Quad};
let subject = NamedNode::new("http://example.org/subject").unwrap();
let predicate = NamedNode::new("http://example.org/predicate").unwrap();
let object = NamedNode::new("http://example.org/object").unwrap();
let quad = Quad::new(subject, predicate, object, GraphName::DefaultGraph);
let statement = Statement::from(quad.clone());
let metadata = SnapshotMetadata {
backward_compatible: true,
description: "Test snapshot with triples".to_string(),
sectors: vec!["test".to_string()],
tags: BTreeMap::new(),
};
let snap = SigmaSnapshot::new(
None,
vec![statement],
"0.1.0".to_string(),
"test_signature".to_string(),
metadata,
);
let dataset = snap.as_dataset().unwrap();
assert_eq!(dataset.len(), 1);
}
#[test]
fn test_statement_round_trip_default_graph() {
use oxigraph::model::{GraphName, NamedNode, Quad};
let subject = NamedNode::new("http://example.org/subject").unwrap();
let predicate = NamedNode::new("http://example.org/predicate").unwrap();
let object = NamedNode::new("http://example.org/object").unwrap();
let original_quad = Quad::new(
subject.clone(),
predicate.clone(),
object.clone(),
GraphName::DefaultGraph,
);
let statement = Statement::from(original_quad.clone());
assert!(statement.graph.is_none());
let metadata = SnapshotMetadata {
backward_compatible: true,
description: "Test".to_string(),
sectors: vec!["test".to_string()],
tags: BTreeMap::new(),
};
let snap = SigmaSnapshot::new(
None,
vec![statement],
"0.1.0".to_string(),
"test_signature".to_string(),
metadata,
);
let dataset = snap.as_dataset().unwrap();
assert_eq!(dataset.len(), 1);
}
#[test]
fn test_statement_round_trip_named_graph() {
use oxigraph::model::{GraphName, NamedNode, Quad};
let subject = NamedNode::new("http://example.org/subject").unwrap();
let predicate = NamedNode::new("http://example.org/predicate").unwrap();
let object = NamedNode::new("http://example.org/object").unwrap();
let graph_name = NamedNode::new("http://example.org/graph").unwrap();
let original_quad = Quad::new(
subject.clone(),
predicate.clone(),
object.clone(),
GraphName::NamedNode(graph_name.clone()),
);
let statement = Statement::from(original_quad.clone());
assert!(statement.graph.is_some());
assert_eq!(
statement.graph.as_ref().unwrap(),
"http://example.org/graph"
);
let metadata = SnapshotMetadata {
backward_compatible: true,
description: "Test".to_string(),
sectors: vec!["test".to_string()],
tags: BTreeMap::new(),
};
let snap = SigmaSnapshot::new(
None,
vec![statement],
"0.1.0".to_string(),
"test_signature".to_string(),
metadata,
);
let dataset = snap.as_dataset().unwrap();
assert_eq!(dataset.len(), 1);
}
#[test]
fn test_as_dataset_invalid_iri() {
let invalid_statement = Statement {
subject: "not a valid IRI".to_string(),
predicate: "http://example.org/predicate".to_string(),
object: "http://example.org/object".to_string(),
graph: None,
};
let metadata = SnapshotMetadata {
backward_compatible: true,
description: "Test".to_string(),
sectors: vec!["test".to_string()],
tags: BTreeMap::new(),
};
let snap = SigmaSnapshot::new(
None,
vec![invalid_statement],
"0.1.0".to_string(),
"test_signature".to_string(),
metadata,
);
let result = snap.as_dataset();
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Invalid subject IRI"));
}
}