diff options
Diffstat (limited to 'src/remake/kind.rs')
| -rw-r--r-- | src/remake/kind.rs | 114 |
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. |
