use std::path::Path;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use uuid::Uuid;
use crate::Endpoint;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WatchDecision {
Apply,
Unchanged,
SuppressedOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OriginStamp {
origin_id: Uuid,
content_hash: String,
}
impl OriginStamp {
#[must_use]
pub fn new(origin_id: Uuid, content_hash: impl Into<String>) -> Self {
Self {
origin_id,
content_hash: content_hash.into(),
}
}
#[must_use]
pub const fn origin_id(&self) -> Uuid {
self.origin_id
}
#[must_use]
pub fn matches(&self, content_hash: &str) -> bool {
self.content_hash == content_hash
}
pub fn save(&self, path: &Path) -> Result<(), WatchError> {
save_json(self, path)
}
pub fn load(path: &Path) -> Result<Self, WatchError> {
load_json(path)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WatchState {
origin_id: Uuid,
source: Endpoint,
target: Endpoint,
last_applied_hash: Option<String>,
}
impl WatchState {
#[must_use]
pub fn new(source: Endpoint, target: Endpoint) -> Self {
let origin_id = relationship_id(&source, &target);
Self {
origin_id,
source,
target,
last_applied_hash: None,
}
}
#[must_use]
pub const fn origin_id(&self) -> Uuid {
self.origin_id
}
#[must_use]
pub fn source(&self) -> &Endpoint {
&self.source
}
#[must_use]
pub fn target(&self) -> &Endpoint {
&self.target
}
#[must_use]
pub fn decide(&self, hash: &str, incoming_origin: Option<Uuid>) -> WatchDecision {
if incoming_origin == Some(self.origin_id) {
WatchDecision::SuppressedOrigin
} else if self.last_applied_hash.as_deref() == Some(hash) {
WatchDecision::Unchanged
} else {
WatchDecision::Apply
}
}
pub fn record_applied(&mut self, hash: impl Into<String>) {
self.last_applied_hash = Some(hash.into());
}
pub fn save(&self, path: &Path) -> Result<(), WatchError> {
save_json(self, path)
}
pub fn load(path: &Path) -> Result<Self, WatchError> {
load_json(path)
}
}
fn relationship_id(source: &Endpoint, target: &Endpoint) -> Uuid {
let source = source.to_string();
let target = target.to_string();
let (first, second) = if source <= target {
(source, target)
} else {
(target, source)
};
let digest = Sha256::digest(format!("{first}\0{second}").as_bytes());
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest[..16]);
Uuid::from_bytes(bytes)
}
#[must_use]
pub fn content_hash(content: &[u8]) -> String {
hex::encode(Sha256::digest(content))
}
fn save_json(value: &impl Serialize, path: &Path) -> Result<(), WatchError> {
let parent = path.parent().ok_or(WatchError::MissingParent)?;
std::fs::create_dir_all(parent)?;
let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
serde_json::to_writer_pretty(temporary.as_file_mut(), value)?;
temporary.as_file().sync_all()?;
temporary.persist(path).map_err(|error| error.error)?;
Ok(())
}
fn load_json<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T, WatchError> {
let bytes = std::fs::read(path)?;
serde_json::from_slice(&bytes).map_err(Into::into)
}
#[derive(Debug, Error)]
pub enum WatchError {
#[error("watch state I/O failed")]
Io(#[from] std::io::Error),
#[error("watch state JSON is invalid")]
Json(#[from] serde_json::Error),
#[error("watch state path has no parent directory")]
MissingParent,
}