use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::compatibility::{Epoch, RepositoryFacts};
use super::converge::Entry;
use super::model::{RepositoryId, WorkspaceId};
use super::scan::PolicyDigest;
pub const VERSION: u32 = 7;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceSummary {
pub workspace: WorkspaceId,
pub display_name: String,
pub root_commit: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryReport {
pub facts: RepositoryFacts,
pub policy: PolicyDigest,
pub entries: BTreeMap<String, Entry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "request", rename_all = "snake_case")]
pub enum Request {
Inventory,
Report {
workspace: WorkspaceId,
repositories: Vec<RepositoryId>,
},
Fetch {
workspace: WorkspaceId,
repository: RepositoryId,
epoch: Epoch,
paths: Vec<String>,
},
Apply {
workspace: WorkspaceId,
repository: RepositoryId,
epoch: Epoch,
writes: Vec<Write>,
deletions: Vec<String>,
found: BTreeMap<String, Entry>,
baseline_digest: String,
},
Confirm {
workspace: WorkspaceId,
repository: RepositoryId,
baseline_digest: String,
},
Acknowledge {
workspace: WorkspaceId,
repository: RepositoryId,
epoch: Epoch,
paths: Vec<String>,
baseline_digest: String,
},
Plugin {
service: crate::plugin::PluginService,
body: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Write {
pub path: String,
pub entry: Entry,
pub content: String,
}
impl Write {
pub fn decoded(&self) -> Result<Vec<u8>, String> {
let bytes = super::identity::base64_decode(&self.content)
.map_err(|e| format!("{}: {e}", self.path))?;
let actual = super::converge::hash_bytes(&bytes);
if actual != self.entry.hash {
return Err(format!(
"{}: the content does not match the hash it was sent with",
self.path
));
}
if bytes.len() as u64 != self.entry.size {
return Err(format!(
"{}: the content is {} bytes and was sent as {}",
self.path,
bytes.len(),
self.entry.size
));
}
Ok(bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "response", rename_all = "snake_case")]
pub enum Response {
Inventory {
workspaces: Vec<WorkspaceSummary>,
},
Report {
#[serde(skip_serializing_if = "Option::is_none")]
workspace_present: Option<bool>,
repositories: Vec<RepositoryReport>,
},
Fetched {
writes: Vec<Write>,
},
Applied {
applied: Vec<String>,
},
Acknowledged,
Plugin {
body: String,
},
Refused {
code: RefusalCode,
detail: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RefusalCode {
EpochMoved,
BaselineMismatch,
UnknownWorkspace,
UnknownRepository,
NotEnabled,
ServiceOff,
ForbiddenPath,
PolicyMismatch,
Malformed,
Internal,
NoSuchService,
PluginAbsent,
PluginRefused,
}
impl RefusalCode {
pub fn as_str(self) -> &'static str {
match self {
Self::EpochMoved => "epoch_moved",
Self::BaselineMismatch => "baseline_mismatch",
Self::UnknownWorkspace => "unknown_workspace",
Self::UnknownRepository => "unknown_repository",
Self::NotEnabled => "not_enabled",
Self::ServiceOff => "service_off",
Self::ForbiddenPath => "forbidden_path",
Self::PolicyMismatch => "policy_mismatch",
Self::Malformed => "malformed",
Self::Internal => "internal",
Self::NoSuchService => "no_such_service",
Self::PluginAbsent => "plugin_absent",
Self::PluginRefused => "plugin_refused",
}
}
}
pub fn baseline_digest(baseline: &super::converge::Baseline) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"wsb-peer-baseline-v1");
for (path, entry) in baseline {
hasher.update(path.as_bytes());
hasher.update([0]);
hasher.update(entry.hash.as_bytes());
hasher.update([0]);
hasher.update(match entry.kind {
super::converge::EntryKind::File => b"f".as_slice(),
super::converge::EntryKind::Symlink => b"l".as_slice(),
});
hasher.update(if entry.executable { b"x" } else { b"-" });
hasher.update([0]);
}
format!("{:x}", hasher.finalize())
}
#[cfg(test)]
#[path = "protocol_tests.rs"]
mod tests;