use crate::constants::MEMVID_TICKET_PUBKEY;
use crate::error::{MemvidError, Result};
use crate::memvid::lifecycle::Memvid;
use crate::signature::{parse_ed25519_public_key_base64, verify_ticket_signature};
use crate::types::{FrameStatus, SignedTicket, Stats, Ticket, TicketRef};
impl Memvid {
pub fn stats(&self) -> Result<Stats> {
let metadata = self.file.metadata()?;
let mut payload_bytes = 0u64;
let mut logical_bytes = 0u64;
let mut active_frames = 0u64;
for frame in self
.toc
.frames
.iter()
.filter(|frame| frame.status == FrameStatus::Active)
{
active_frames = active_frames.saturating_add(1);
let stored = frame.payload_length;
payload_bytes = payload_bytes.saturating_add(stored);
if stored > 0 {
let logical = frame.canonical_length.unwrap_or(stored);
logical_bytes = logical_bytes.saturating_add(logical);
}
}
let saved_bytes = logical_bytes.saturating_sub(payload_bytes);
let round2 = |value: f64| (value * 100.0).round() / 100.0;
let compression_ratio_percent = if logical_bytes > 0 {
round2((payload_bytes as f64 / logical_bytes as f64) * 100.0)
} else {
100.0
};
let savings_percent = if logical_bytes > 0 {
round2((saved_bytes as f64 / logical_bytes as f64) * 100.0)
} else {
0.0
};
let storage_utilisation_percent = if self.capacity_limit() > 0 {
round2((metadata.len() as f64 / self.capacity_limit() as f64) * 100.0)
} else {
0.0
};
let remaining_capacity_bytes = self.capacity_limit().saturating_sub(metadata.len());
let average_payload = if active_frames > 0 {
payload_bytes / active_frames
} else {
0
};
let average_logical = if active_frames > 0 {
logical_bytes / active_frames
} else {
0
};
let wal_bytes = self.header.wal_size;
let mut lex_index_bytes = 0u64;
if let Some(ref lex) = self.toc.indexes.lex {
lex_index_bytes = lex_index_bytes.saturating_add(lex.bytes_length);
}
for seg in &self.toc.indexes.lex_segments {
lex_index_bytes = lex_index_bytes.saturating_add(seg.bytes_length);
}
let mut vec_index_bytes = 0u64;
let mut vector_count = 0u64;
if let Some(ref vec) = self.toc.indexes.vec {
vec_index_bytes = vec_index_bytes.saturating_add(vec.bytes_length);
vector_count = vector_count.saturating_add(vec.vector_count);
}
for seg in &self.toc.segment_catalog.vec_segments {
vec_index_bytes = vec_index_bytes.saturating_add(seg.common.bytes_length);
vector_count = vector_count.saturating_add(seg.vector_count);
}
let mut time_index_bytes = 0u64;
if let Some(ref time) = self.toc.time_index {
time_index_bytes = time_index_bytes.saturating_add(time.bytes_length);
}
for seg in &self.toc.segment_catalog.time_segments {
time_index_bytes = time_index_bytes.saturating_add(seg.common.bytes_length);
}
let clip_image_count = self.toc.indexes.clip.as_ref().map_or(0, |c| c.vector_count);
Ok(Stats {
frame_count: self.toc.frames.len() as u64,
size_bytes: metadata.len(),
tier: self.tier(),
has_lex_index: crate::memvid::lifecycle::has_lex_index(&self.toc),
has_vec_index: self.toc.indexes.vec.is_some()
|| !self.toc.segment_catalog.vec_segments.is_empty(),
has_clip_index: self.toc.indexes.clip.is_some(),
has_time_index: self.toc.time_index.is_some()
|| !self.toc.segment_catalog.time_segments.is_empty(),
seq_no: (self.toc.ticket_ref.seq_no != 0).then_some(self.toc.ticket_ref.seq_no),
capacity_bytes: self.capacity_limit(),
active_frame_count: active_frames,
payload_bytes,
logical_bytes,
saved_bytes,
compression_ratio_percent,
savings_percent,
storage_utilisation_percent,
remaining_capacity_bytes,
average_frame_payload_bytes: average_payload,
average_frame_logical_bytes: average_logical,
wal_bytes,
lex_index_bytes,
vec_index_bytes,
time_index_bytes,
vector_count,
clip_image_count,
lex_enabled: self.lex_enabled,
vec_enabled: self.vec_enabled,
})
}
#[deprecated(
since = "0.3.0",
note = "Use apply_signed_ticket() for cryptographically verified tickets"
)]
pub fn apply_ticket(&mut self, ticket: Ticket) -> Result<()> {
self.ensure_writable()?;
let current_seq = self.toc.ticket_ref.seq_no;
if ticket.seq_no <= current_seq {
return Err(MemvidError::TicketSequence {
expected: current_seq + 1,
actual: ticket.seq_no,
});
}
self.toc.ticket_ref.capacity_bytes = ticket.capacity_bytes.unwrap_or(0);
self.toc.ticket_ref.issuer = ticket.issuer;
self.toc.ticket_ref.seq_no = ticket.seq_no;
self.toc.ticket_ref.expires_in_secs = ticket.expires_in_secs;
self.toc.ticket_ref.verified = false;
self.generation = self.generation.wrapping_add(1);
self.rewrite_toc_footer()?;
self.header.toc_checksum = self.toc.toc_checksum;
crate::persist_header(&mut self.file, &self.header)?;
self.file.sync_all()?;
Ok(())
}
pub fn apply_signed_ticket(&mut self, ticket: SignedTicket) -> Result<()> {
self.ensure_writable()?;
let verifying_key = parse_ed25519_public_key_base64(MEMVID_TICKET_PUBKEY)?;
let binding = self.toc.memory_binding.as_ref().ok_or_else(|| {
MemvidError::TicketSignatureInvalid {
reason: "cannot apply signed ticket: memory is not bound to the Memvid API".into(),
}
})?;
if ticket.memory_id != binding.memory_id {
return Err(MemvidError::TicketSignatureInvalid {
reason: format!(
"ticket memory_id {} does not match this memory {}",
ticket.memory_id, binding.memory_id
)
.into_boxed_str(),
});
}
verify_ticket_signature(
&verifying_key,
&ticket.memory_id,
&ticket.issuer,
ticket.seq_no,
ticket.expires_in_secs,
ticket.capacity_bytes,
&ticket.signature,
)?;
let current_seq = self.toc.ticket_ref.seq_no;
if ticket.seq_no <= current_seq {
return Err(MemvidError::TicketSequence {
expected: current_seq + 1,
actual: ticket.seq_no,
});
}
self.toc.ticket_ref.capacity_bytes = ticket.capacity_bytes.unwrap_or(0);
self.toc.ticket_ref.issuer = ticket.issuer;
self.toc.ticket_ref.seq_no = ticket.seq_no;
self.toc.ticket_ref.expires_in_secs = ticket.expires_in_secs;
self.toc.ticket_ref.verified = true;
self.generation = self.generation.wrapping_add(1);
self.rewrite_toc_footer()?;
self.header.toc_checksum = self.toc.toc_checksum;
crate::persist_header(&mut self.file, &self.header)?;
self.file.sync_all()?;
Ok(())
}
#[must_use]
pub fn current_ticket(&self) -> TicketRef {
self.toc.ticket_ref.clone()
}
#[must_use]
pub fn logic_mesh_manifest(&self) -> Option<&crate::types::LogicMeshManifest> {
self.toc.logic_mesh.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pubkey_parses() {
let result = parse_ed25519_public_key_base64(MEMVID_TICKET_PUBKEY);
assert!(result.is_ok(), "Failed to parse embedded public key");
}
#[test]
fn test_signed_ticket_struct() {
let memory_id = uuid::Uuid::new_v4();
let signature = vec![0u8; 64];
let ticket = SignedTicket::new(
"memvid.com",
1,
86400,
Some(100 * 1024 * 1024),
memory_id,
signature.clone(),
);
assert_eq!(ticket.issuer, "memvid.com");
assert_eq!(ticket.seq_no, 1);
assert_eq!(ticket.expires_in_secs, 86400);
assert_eq!(ticket.capacity_bytes, Some(100 * 1024 * 1024));
assert_eq!(ticket.memory_id, memory_id);
assert_eq!(ticket.signature, signature);
}
#[test]
fn test_signed_ticket_serialization() {
let memory_id = uuid::Uuid::nil();
let signature = vec![1u8; 64];
let ticket = SignedTicket::new("test", 5, 3600, None, memory_id, signature);
let json = serde_json::to_string(&ticket).expect("serialization failed");
assert!(json.contains("\"issuer\":\"test\""));
assert!(json.contains("\"seq_no\":5"));
let parsed: SignedTicket = serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(parsed.issuer, ticket.issuer);
assert_eq!(parsed.seq_no, ticket.seq_no);
assert_eq!(parsed.memory_id, ticket.memory_id);
}
#[test]
fn test_ticket_ref_verified_default() {
let ticket_ref: TicketRef = serde_json::from_str(
r#"{"issuer":"test","seq_no":1,"expires_in_secs":0,"capacity_bytes":0}"#,
)
.expect("deserialization failed");
assert!(!ticket_ref.verified, "verified should default to false");
}
#[test]
fn test_invalid_signature_rejected() {
let memory_id = uuid::Uuid::new_v4();
let invalid_signature = vec![0u8; 64];
let ticket = SignedTicket::new(
"memvid.com",
1,
86400,
Some(100 * 1024 * 1024),
memory_id,
invalid_signature,
);
let verifying_key = parse_ed25519_public_key_base64(MEMVID_TICKET_PUBKEY).unwrap();
let result = verify_ticket_signature(
&verifying_key,
&ticket.memory_id,
&ticket.issuer,
ticket.seq_no,
ticket.expires_in_secs,
ticket.capacity_bytes,
&ticket.signature,
);
assert!(result.is_err(), "Invalid signature should be rejected");
if let Err(MemvidError::TicketSignatureInvalid { reason }) = result {
assert!(
reason.contains("signature"),
"Error should mention signature"
);
}
}
}