use super::{EVIDENCE_ADMISSION_VERDICT_REVISION, EvidenceProducerKind, StoredEvidenceSpan};
use crate::cass::transcript::{MAX_SOURCE_BYTES, TRANSCRIPT_PROJECTION_VERSION};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvidenceReaderProjection {
projection_version: u32,
source_binding: String,
reader_body: String,
reader_text: String,
egress_safe: bool,
integrity_binding: String,
}
impl EvidenceReaderProjection {
pub(super) fn derive(span: &StoredEvidenceSpan) -> Self {
let start = span.excerpt.trim_start();
let structured_cass = EvidenceProducerKind::parse(&span.producer_kind)
== Some(EvidenceProducerKind::CassImport)
&& (start.starts_with('{') || start.starts_with('['));
let (reader_body, reader_text) = if structured_cass {
crate::cass::transcript::materialized_reader_views(&span.excerpt).unwrap_or_default()
} else {
(span.excerpt.clone(), span.excerpt.clone())
};
let egress = crate::policy::screen_external_text_for_ingestion(&span.excerpt);
let egress_safe = !egress.redacted
&& !egress.instruction_like
&& matches!(egress.instruction_risk, "none" | "low");
let mut projection = Self {
projection_version: TRANSCRIPT_PROJECTION_VERSION,
source_binding: source_binding(span),
reader_body,
reader_text,
egress_safe,
integrity_binding: String::new(),
};
projection.integrity_binding = projection.calculate_integrity_binding();
projection
}
pub(super) fn from_storage(
span: &StoredEvidenceSpan,
projection_version: u32,
source_binding: String,
reader_body: String,
reader_text: String,
egress_safe: bool,
integrity_binding: String,
) -> Option<Self> {
if reader_body.len() > MAX_SOURCE_BYTES
|| reader_text.len() > MAX_SOURCE_BYTES
|| reader_body.is_empty() != reader_text.is_empty()
{
return None;
}
let projection = Self {
projection_version,
source_binding,
reader_body,
reader_text,
egress_safe,
integrity_binding,
};
(projection.is_current_for(span)
&& projection.integrity_binding == projection.calculate_integrity_binding())
.then_some(projection)
}
pub(super) fn is_current_for(&self, span: &StoredEvidenceSpan) -> bool {
self.projection_version == TRANSCRIPT_PROJECTION_VERSION
&& self.source_binding == source_binding(span)
}
pub(super) fn projection_version(&self) -> u32 {
self.projection_version
}
pub(super) fn source_binding(&self) -> &str {
&self.source_binding
}
pub(super) fn reader_body(&self) -> &str {
&self.reader_body
}
pub(super) fn reader_text(&self) -> &str {
&self.reader_text
}
pub(super) fn egress_safe(&self) -> bool {
self.egress_safe
}
pub(super) fn integrity_binding(&self) -> &str {
&self.integrity_binding
}
fn calculate_integrity_binding(&self) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(b"ee.evidence.reader_projection.content.v1");
hasher.update(&self.projection_version.to_le_bytes());
hasher.update(&[u8::from(self.egress_safe)]);
for text in [&self.source_binding, &self.reader_body, &self.reader_text] {
hasher.update(&(text.len() as u64).to_le_bytes());
hasher.update(text.as_bytes());
}
format!("blake3:{}", hasher.finalize().to_hex())
}
}
fn source_binding(span: &StoredEvidenceSpan) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(b"ee.evidence.reader_projection.source.v1");
hasher.update(&TRANSCRIPT_PROJECTION_VERSION.to_le_bytes());
hasher.update(
span.admission_verdict_hasher(EVIDENCE_ADMISSION_VERDICT_REVISION)
.finalize()
.as_bytes(),
);
format!("blake3:{}", hasher.finalize().to_hex())
}