use crate::model::{
artifacts::ArtifactChecksumRecord,
snapshot_read::{SnapshotReadObservation, SnapshotReadRequest, SnapshotVisibility},
};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SnapshotReadPath {
Controller,
Public,
AllowedViewer,
}
#[derive(Clone, Debug)]
pub struct SnapshotReadView<'a> {
request: ArtifactChecksumRecord,
observation: &'a SnapshotReadObservation,
path: SnapshotReadPath,
}
impl SnapshotReadView<'_> {
#[must_use]
pub const fn request(&self) -> &ArtifactChecksumRecord {
&self.request
}
#[must_use]
pub fn target(&self) -> &str {
self.observation.target()
}
#[must_use]
pub const fn visibility(&self) -> &SnapshotVisibility {
self.observation.visibility()
}
#[must_use]
pub const fn path(&self) -> SnapshotReadPath {
self.path
}
#[must_use]
pub const fn evidence(&self) -> &ArtifactChecksumRecord {
self.observation.evidence()
}
#[must_use]
pub const fn remote_observations(&self) -> u32 {
self.observation.remote_observations()
}
}
pub fn validate<'a>(
request: &SnapshotReadRequest<'_>,
observation: &'a SnapshotReadObservation,
) -> Result<SnapshotReadView<'a>, SnapshotReadError> {
let digest = request.digest();
if *observation.request() != digest {
return Err(SnapshotReadError::RequestMismatch);
}
let binding = request.binding();
for (field, expected, actual) in [
(
"network",
binding.network(),
observation.context().network(),
),
("caller", binding.caller(), observation.context().caller()),
(
"release",
binding.release(),
observation.context().release(),
),
] {
if actual != expected {
return Err(SnapshotReadError::ContextMismatch(field));
}
}
if observation.target() != binding.target() {
return Err(SnapshotReadError::TargetMismatch);
}
if observation.remote_observations() > request.max_remote_observations() {
return Err(SnapshotReadError::ObservationLimitExceeded {
limit: request.max_remote_observations(),
reported: observation.remote_observations(),
});
}
let controller = observation
.controllers()
.is_some_and(|set| set.contains_caller(binding));
let path = if controller {
SnapshotReadPath::Controller
} else {
match observation.visibility() {
SnapshotVisibility::Public => SnapshotReadPath::Public,
SnapshotVisibility::AllowedViewers(viewers) if viewers.contains_caller(binding) => {
SnapshotReadPath::AllowedViewer
}
SnapshotVisibility::Controllers | SnapshotVisibility::AllowedViewers(_) => {
return Err(if observation.controllers().is_none() {
SnapshotReadError::ControllersUnobserved
} else {
SnapshotReadError::CallerCannotRead
});
}
}
};
Ok(SnapshotReadView {
request: digest,
observation,
path,
})
}
#[derive(Debug, Eq, Error, PartialEq)]
pub enum SnapshotReadError {
#[error("snapshot read request mismatch")]
RequestMismatch,
#[error("snapshot read observed {0} mismatch")]
ContextMismatch(&'static str),
#[error("snapshot read observed target mismatch")]
TargetMismatch,
#[error("snapshot read requires unobserved controller evidence")]
ControllersUnobserved,
#[error("selected caller cannot read observed snapshots")]
CallerCannotRead,
#[error("snapshot read reports {reported} observations above ceiling {limit}")]
ObservationLimitExceeded {
limit: u32,
reported: u32,
},
}
#[cfg(test)]
mod tests;