aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake/kind.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/kind.rs')
-rw-r--r--src/remake/kind.rs114
1 files changed, 110 insertions, 4 deletions
diff --git a/src/remake/kind.rs b/src/remake/kind.rs
index a8d80d8..a429828 100644
--- a/src/remake/kind.rs
+++ b/src/remake/kind.rs
@@ -11,11 +11,13 @@
//! ([`RemakeKind::translate`]).
//!
//! Kinds: [`Compact`] (same content, no history), [`Replace`] (an operator's document
-//! in store form, with rules for where later writes go) and [`Import`] (the same with
-//! an Automerge document, e.g. one exported with `GET /admin/document`).
+//! in store form, with rules for where later writes go), [`Import`] (the same with
+//! an Automerge document, e.g. one exported with `GET /admin/document`) and [`Strict`]
+//! (compaction turning the swarm strict, keeping only the signed values a manifest
+//! names).
//!
-//! Protocol data under `_continuum` is not a kind's business: the generic part carries
-//! it forward and replays it unchanged.
+//! Protocol data under `_continuum` is the generic part's: it carries it forward and
+//! replays it unchanged; a strict remake alone changes it ([`RemakeKind::protocol`]).
use std::path::Path;
@@ -77,6 +79,17 @@ pub(crate) trait RemakeKind: Send + Sync + 'static {
/// Every node: where a user path written after the base goes.
fn translate(&self, path: &[String], translation: &JsonValue) -> Translation;
+ /// Every node: changes the protocol data carried into `target` from `old_at_base`.
+ /// Deterministic too. Nothing, for every kind but [`Strict`].
+ fn protocol(
+ &self,
+ _old_at_base: &Document,
+ _content: Option<&[u8]>,
+ _target: &mut Transaction<'_>,
+ ) -> Result<(), StoreError> {
+ Ok(())
+ }
+
/// Whether the coordinator fixes the base when it commits instead of when it
/// proposes (no `baseHeads` then). Right for a kind whose content is the old
/// document at the base: the later the base, the fewer writes every switching node
@@ -92,6 +105,7 @@ pub(crate) fn by_name(name: &str) -> Option<&'static dyn RemakeKind> {
"compact" => Some(&Compact),
"replace" => Some(&Replace),
"import" => Some(&Import),
+ "strict" => Some(&Strict),
_ => None,
}
}
@@ -180,6 +194,98 @@ impl RemakeKind for Compact {
}
}
+/// Turning strict (`access::strict`): the same content, no history, as [`Compact`],
+/// with the strict policy of the manifest, and at the places values are signed only the
+/// values it names, as they were at the base. The manifest is the content; every node
+/// checks its signature against its `--strict-keys` before it votes (`remake::vote`),
+/// but builds from it unchecked, as all do alike.
+///
+/// Spec: `{"manifest": "<its line>\n<its signature>"}`. The base is the proposal's
+/// (`baseHeads`, those the manifest was made at).
+pub(crate) struct Strict;
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct StrictSpec {
+ manifest: String,
+}
+
+impl Strict {
+ fn manifest(content: Option<&[u8]>) -> Result<crate::access::strict::Manifest, StoreError> {
+ let content = content.ok_or_else(|| anyhow!("strict without its manifest"))?;
+ let text = std::str::from_utf8(content).map_err(|_| anyhow!("manifest not UTF-8"))?;
+ crate::access::strict::read(text).map_err(|why| anyhow!(why).into())
+ }
+
+ /// Removes from `target` what `old_at_base` holds at the signed places the
+ /// manifest doesn't name, of those `wanted` picks.
+ fn keep_named(
+ old_at_base: &Document,
+ manifest: &crate::access::strict::Manifest,
+ target: &mut Transaction<'_>,
+ wanted: impl Fn(&[String]) -> bool,
+ ) -> Result<(), StoreError> {
+ for (path, value) in crate::access::strict::held(old_at_base) {
+ if wanted(&path) && !manifest.keeps(&path, &value) {
+ match store::delete_path(target, &path, false) {
+ Ok(()) | Err(StoreError::NotFound) => {}
+ Err(error) => return Err(error),
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+impl RemakeKind for Strict {
+ fn name(&self) -> &'static str {
+ "strict"
+ }
+
+ fn propose(&self, spec: &JsonValue) -> Result<Prepared, String> {
+ let spec: StrictSpec =
+ serde_json::from_value(spec.clone()).map_err(|error| error.to_string())?;
+ crate::access::strict::read(&spec.manifest)?;
+ Ok(Prepared {
+ content: Some(spec.manifest.into_bytes()),
+ translation: JsonValue::Null,
+ })
+ }
+
+ fn build(
+ &self,
+ old_at_base: &Document,
+ content: Option<&[u8]>,
+ target: &mut Transaction<'_>,
+ ) -> Result<(), StoreError> {
+ let manifest = Self::manifest(content)?;
+ store::materialize_into(old_at_base, target, &meta::is_protocol)?;
+ Self::keep_named(old_at_base, &manifest, target, |path| {
+ !meta::is_protocol(path)
+ })
+ }
+
+ fn translate(&self, _path: &[String], _translation: &JsonValue) -> Translation {
+ // Judged by the strict policy, as every replay is (`remake::delta`).
+ Translation::Same
+ }
+
+ fn protocol(
+ &self,
+ old_at_base: &Document,
+ content: Option<&[u8]>,
+ target: &mut Transaction<'_>,
+ ) -> Result<(), StoreError> {
+ let manifest = Self::manifest(content)?;
+ Self::keep_named(old_at_base, &manifest, target, meta::is_protocol)?;
+ store::write_path(
+ target,
+ &meta::path(&[meta::ACCESS, "policy"]),
+ &canonical(manifest.policy),
+ )
+ }
+}
+
/// Replacement: the new document is supplied by an operator, in store form (the JSON
/// `GET /store` returns), and may be structured entirely differently. Writes made after
/// the base go where the remake's rules say; a path no rule covers is quarantined.