diff options
| author | tv <tv@krebsco.de> | 2026-10-26 04:13:11 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-26 04:13:11 +0100 |
| commit | d668ce2ff8f308e54c1c7a2c145cb3479cab899d (patch) | |
| tree | 851ceb314782a9428cac36d48fb5abc5805ead5f /src | |
| parent | c13cee08dedeb8e73560f9bf9f7154c9fa089e2d (diff) | |
move protocol paths to dedicated module
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 7 | ||||
| -rw-r--r-- | src/document/meta.rs | 45 | ||||
| -rw-r--r-- | src/document/mod.rs | 1 | ||||
| -rw-r--r-- | src/remake/delta.rs | 19 | ||||
| -rw-r--r-- | src/remake/kind.rs | 25 | ||||
| -rw-r--r-- | src/remake/records.rs | 9 | ||||
| -rw-r--r-- | src/remake/snapshot.rs | 33 | ||||
| -rw-r--r-- | src/remake/switch.rs | 5 | ||||
| -rw-r--r-- | src/swarm/bootstrap.rs | 5 | ||||
| -rw-r--r-- | src/swarm/members.rs | 5 | ||||
| -rw-r--r-- | src/swarm/peer.rs | 4 |
11 files changed, 96 insertions, 62 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index affb9ef..bec7764 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -25,6 +25,7 @@ use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use crate::api::auth::{AuthState, AuthenticatedUser, basic_auth}; +use crate::document::meta; use crate::document::store::StoreError; use crate::state::AppState; @@ -117,13 +118,11 @@ fn parse_write_path( method: &str, ) -> Result<Vec<String>, StatusCode> { let path = parse_key_path(key)?; - if path.first().map(String::as_str) != Some(crate::remake::snapshot::META_ROOT) { + if !meta::is_protocol(&path) { return Ok(path); } // `_continuum` itself, epoch metadata and the swarm ID: nobody. - let fixed = path.len() < 2 - || path[1] == crate::remake::snapshot::EPOCHS - || path[1] == crate::remake::snapshot::SWARM; + let fixed = path.len() < 2 || path[1] == meta::EPOCHS || path[1] == meta::SWARM; if fixed || !state.protocol_writers.contains(&user.0) { warn!(user = %user.0, method, path = %key, "Refused a write to protocol data"); return Err(StatusCode::FORBIDDEN); diff --git a/src/document/meta.rs b/src/document/meta.rs new file mode 100644 index 0000000..482b421 --- /dev/null +++ b/src/document/meta.rs @@ -0,0 +1,45 @@ +//! The protocol's own data in the document: everything under `_continuum`. The names +//! of its parts, and the paths to them, are defined here only. +//! +//! * `members/<pk>`: the member registry (`swarm::members`). +//! * `forgotten/<pk>`: forget records (`swarm::peer`). +//! * `remakes/<id>`: remake records (`remake::records`). +//! * `votes/<pk>`: the votes register (`remake::records`). +//! * `epochs/<generation>`: epoch metadata, written by snapshots (`remake::snapshot`). +//! * `swarm`: the swarm ID, written once by the genesis node (`swarm::bootstrap`). + +/// The key every protocol path starts with. +pub(crate) const ROOT: &str = "_continuum"; +pub(crate) const MEMBERS: &str = "members"; +pub(crate) const FORGOTTEN: &str = "forgotten"; +pub(crate) const REMAKES: &str = "remakes"; +pub(crate) const VOTES: &str = "votes"; +pub(crate) const EPOCHS: &str = "epochs"; +pub(crate) const SWARM: &str = "swarm"; + +/// The store path `_continuum/<parts…>`. +pub(crate) fn path(parts: &[&str]) -> Vec<String> { + std::iter::once(ROOT) + .chain(parts.iter().copied()) + .map(str::to_owned) + .collect() +} + +/// Whether a store path is protocol data (under `_continuum`). +pub(crate) fn is_protocol(path: &[String]) -> bool { + path.first().is_some_and(|root| root == ROOT) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn paths_start_at_the_root() { + assert_eq!(path(&[]), vec!["_continuum"]); + assert_eq!(path(&[MEMBERS, "pk"]), vec!["_continuum", "members", "pk"]); + assert!(is_protocol(&path(&[VOTES]))); + assert!(!is_protocol(&["users".to_owned()])); + assert!(!is_protocol(&[])); + } +} diff --git a/src/document/mod.rs b/src/document/mod.rs index 0da1d67..1024859 100644 --- a/src/document/mod.rs +++ b/src/document/mod.rs @@ -2,5 +2,6 @@ //! (merged-view paths over JSON) the HTTP API and remakes work with. pub(crate) mod file; +pub(crate) mod meta; pub mod replica; pub mod store; diff --git a/src/remake/delta.rs b/src/remake/delta.rs index 59acc8c..ef6a60a 100644 --- a/src/remake/delta.rs +++ b/src/remake/delta.rs @@ -27,7 +27,7 @@ use automerge::{ActorId, AutoCommit, ChangeHash, PatchAction, Prop}; use tracing::warn; use super::kind::{Translation, Translator}; -use super::snapshot::{EPOCHS, META_ROOT, REMAKES, VOTES}; +use crate::document::meta::{self, EPOCHS, REMAKES, VOTES}; use crate::document::store::{self, StoreError}; #[derive(Debug, Default, Clone)] @@ -138,12 +138,12 @@ fn is_protocol_path(path: &[String]) -> bool { let [root, part, ..] = path else { return false; }; - root == META_ROOT && [REMAKES, VOTES, EPOCHS].contains(&part.as_str()) + root == meta::ROOT && [REMAKES, VOTES, EPOCHS].contains(&part.as_str()) } /// True for the root and for `_continuum` itself. fn contains_protocol_path(path: &[String]) -> bool { - matches!(path, [] | [_]) && path.first().is_none_or(|root| root == META_ROOT) + matches!(path, [] | [_]) && path.first().is_none_or(|root| root == meta::ROOT) } /// What a late node has that a snapshot lacks. @@ -464,7 +464,7 @@ mod tests { let mut new = AutoCommit::new(); store::write_path( &mut new, - &[META_ROOT.into(), EPOCHS.into(), "1".into()], + &meta::path(&[EPOCHS, "1"]), &json!({ "generation": 1 }), ) .unwrap(); @@ -473,21 +473,16 @@ mod tests { // The old epoch replaces `_continuum` wholesale and writes a remake record. store::write_path( &mut old, - &[META_ROOT.into()], + &meta::path(&[]), &json!({ "forgotten": { "pk": 1 } }), ) .unwrap(); - store::write_path( - &mut old, - &[META_ROOT.into(), REMAKES.into(), "c".into()], - &json!({ "x": 1 }), - ) - .unwrap(); + store::write_path(&mut old, &meta::path(&[REMAKES, "c"]), &json!({ "x": 1 })).unwrap(); old.commit(); let after = old.get_heads(); replay(&mut old, &base, &after, &mut new, &Translator::default()); - let meta = &view(&new)[META_ROOT]; + let meta = &view(&new)[meta::ROOT]; assert_eq!(meta[EPOCHS]["1"]["generation"], json!(1)); assert_eq!(meta["forgotten"]["pk"], json!(1)); assert!(meta.get(REMAKES).is_none()); diff --git a/src/remake/kind.rs b/src/remake/kind.rs index 42bdd88..aa9e978 100644 --- a/src/remake/kind.rs +++ b/src/remake/kind.rs @@ -25,7 +25,8 @@ use base64::engine::{Engine, general_purpose::STANDARD as BASE64}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; -use super::snapshot::{META_ROOT, canonical}; +use super::snapshot::canonical; +use crate::document::meta; use crate::document::store::{self, StoreError}; use crate::files::{FileKind, Files}; @@ -141,7 +142,7 @@ pub(crate) fn remake_genesis( /// Paths owned by the protocol rather than by any kind. pub(crate) fn is_reserved(path: &[String]) -> bool { - path.first().is_some_and(|root| root == META_ROOT) + meta::is_protocol(path) } /// Compaction: the new document has the old one's content at the base, without its @@ -254,9 +255,10 @@ impl RemakeKind for Replace { let JsonValue::Object(document) = &spec.document else { return Err("document must be a JSON object (the store root)".to_owned()); }; - if document.contains_key(META_ROOT) { + if document.contains_key(meta::ROOT) { return Err(format!( - "document must not contain the reserved key {META_ROOT:?}" + "document must not contain the reserved key {:?}", + meta::ROOT )); } @@ -283,7 +285,7 @@ impl RemakeKind for Replace { return Ok(()); // propose only accepts objects }; for (key, value) in document { - if key != META_ROOT { + if key != meta::ROOT { store::write_path(target, &[key], &value)?; } } @@ -311,7 +313,8 @@ fn prepare_rules(specs: Vec<RuleSpec>) -> Result<JsonValue, String> { }; if is_reserved(&from) || to.as_deref().is_some_and(is_reserved) { return Err(format!( - "rules must not touch the reserved key {META_ROOT:?}" + "rules must not touch the reserved key {:?}", + meta::ROOT )); } if rules.iter().any(|existing: &Rule| existing.from == from) { @@ -528,7 +531,7 @@ mod tests { )); // Protocol data is never translated. assert_eq!( - translator.translate(&path(&[META_ROOT, "forgotten", "pk"])), + translator.translate(&path(&[meta::ROOT, meta::FORGOTTEN, "pk"])), Translation::Same ); } @@ -541,7 +544,7 @@ mod tests { fn replace_validates_the_spec() { use serde_json::json; assert!(replace(json!({ "document": [] })).is_err()); - assert!(replace(json!({ "document": { META_ROOT: {} } })).is_err()); + assert!(replace(json!({ "document": { (meta::ROOT): {} } })).is_err()); assert!(replace(json!({ "document": {}, "rules": [{ "from": "a" }] })).is_err()); assert!( replace(json!({ "document": {}, "rules": [{ "from": "a", "to": "b", "drop": true }] })) @@ -551,7 +554,7 @@ mod tests { replace(json!({ "document": {}, "rules": [{ "from": "a//b", "to": "c" }] })).is_err() ); assert!( - replace(json!({ "document": {}, "rules": [{ "from": "a", "to": META_ROOT }] })) + replace(json!({ "document": {}, "rules": [{ "from": "a", "to": (meta::ROOT) }] })) .is_err() ); assert!(replace(json!({ "document": {}, "rules": [{ "from": "a", "drop": true }, { "from": ["a"], "to": "b" }] })).is_err()); @@ -602,7 +605,7 @@ mod tests { source .put(ROOT, "hits", automerge::ScalarValue::counter(3)) .unwrap(); - let meta = source.put_object(ROOT, META_ROOT, ObjType::Map).unwrap(); + let meta = source.put_object(ROOT, meta::ROOT, ObjType::Map).unwrap(); source.put(&meta, "members", "elsewhere").unwrap(); let encoded = BASE64.encode(source.save()); @@ -632,7 +635,7 @@ mod tests { let mut b = build(); assert_eq!(a.get_heads(), b.get_heads()); - assert!(a.get(ROOT, META_ROOT).unwrap().is_none()); + assert!(a.get(ROOT, meta::ROOT).unwrap().is_none()); let (notes, notes_id) = a.get(ROOT, "notes").unwrap().unwrap(); assert!(matches!(notes, automerge::Value::Object(ObjType::Text))); assert_eq!(a.text(¬es_id).unwrap(), "hello"); diff --git a/src/remake/records.rs b/src/remake/records.rs index 82c61a1..119ff0c 100644 --- a/src/remake/records.rs +++ b/src/remake/records.rs @@ -34,7 +34,8 @@ use serde_json::Value as JsonValue; use tracing::warn; use super::kind::DEFAULT_KIND; -use super::snapshot::{META_ROOT, REMAKES, SnapshotRequest, VOTES}; +use super::snapshot::SnapshotRequest; +use crate::document::meta::{self, REMAKES, VOTES}; use crate::document::replica::Epoch; use crate::document::store::{self, StoreError}; @@ -334,7 +335,7 @@ impl Record { // --- document access --------------------------------------------------------- fn records_dir() -> Vec<String> { - vec![META_ROOT.to_owned(), REMAKES.to_owned()] + meta::path(&[REMAKES]) } pub(crate) fn record_path(id: &str, part: &str) -> Vec<String> { @@ -424,12 +425,12 @@ fn contested_decision(doc: &AutoCommit, id: &str) -> Option<Decision> { } fn register_path(voter: &str) -> Vec<String> { - vec![META_ROOT.to_owned(), VOTES.to_owned(), voter.to_owned()] + meta::path(&[VOTES, voter]) } /// The votes register; unreadable entries are skipped with a warning. pub(crate) fn load_register(doc: &AutoCommit) -> Register { - store::keys_at(doc, &[META_ROOT.to_owned(), VOTES.to_owned()]) + store::keys_at(doc, &meta::path(&[VOTES])) .unwrap_or_default() .into_iter() .filter_map(|voter| { diff --git a/src/remake/snapshot.rs b/src/remake/snapshot.rs index 20569e0..301b32f 100644 --- a/src/remake/snapshot.rs +++ b/src/remake/snapshot.rs @@ -30,17 +30,10 @@ use sha2::{Digest, Sha256}; use super::kind::{DEFAULT_KIND, RemakeKind, Translator, by_name}; use super::records::{EpochRecord, Policy}; +use crate::document::meta::{self, EPOCHS, REMAKES, VOTES}; use crate::document::replica::Epoch; use crate::document::store::{self, StoreError}; -pub(crate) const META_ROOT: &str = "_continuum"; -pub(crate) const REMAKES: &str = "remakes"; -/// The votes register (`records`). -pub(crate) const VOTES: &str = "votes"; -pub(crate) const EPOCHS: &str = "epochs"; -/// The swarm ID, written once by the genesis node (`swarm::bootstrap`). -pub(crate) const SWARM: &str = "swarm"; - /// Everything that determines a snapshot besides the base content. pub(crate) struct SnapshotParams<'a> { /// ID of the committed remake; seeds the snapshot actor. @@ -230,7 +223,7 @@ pub(crate) fn verify_document(doc: &mut AutoCommit, epoch: Epoch) -> Result<Epoc /// generation it has metadata for, whose snapshot change is the document's only change /// without dependencies (verified). For a document loaded from a backup. pub(crate) fn epoch_of(doc: &mut AutoCommit) -> Result<Epoch, String> { - let generation = store::keys_at(doc, &[META_ROOT.to_owned(), EPOCHS.to_owned()]) + let generation = store::keys_at(doc, &meta::path(&[EPOCHS])) .map_err(|error| error.to_string())? .iter() .filter_map(|key| key.parse::<u64>().ok()) @@ -270,11 +263,7 @@ pub(crate) fn read_epoch_metadata( /// Path of the metadata record for an epoch generation. pub(crate) fn epoch_metadata_path(generation: u64) -> Vec<String> { - vec![ - META_ROOT.to_owned(), - EPOCHS.to_owned(), - generation.to_string(), - ] + meta::path(&[EPOCHS, generation.to_string().as_str()]) } /// Builds the snapshot of `source` at `params.base_heads`. @@ -303,17 +292,17 @@ pub(crate) fn build( // Protocol data carries forward, whatever the kind: forget records, earlier epochs' // metadata (rejoining nodes need it). Remake records and the votes register describe // the old epoch's protocol and stay behind. - match store::delete_path(&mut snapshot, &[META_ROOT.to_owned()], false) { + match store::delete_path(&mut snapshot, &meta::path(&[]), false) { Ok(()) | Err(StoreError::NotFound) => {} Err(error) => return Err(error.into()), } - let mut protocol_keys = store::keys_at(&base, &[META_ROOT.to_owned()])?; + let mut protocol_keys = store::keys_at(&base, &meta::path(&[]))?; protocol_keys.sort(); for key in protocol_keys .into_iter() .filter(|key| key != REMAKES && key != VOTES) { - store::copy_path(&base, &mut snapshot, &[META_ROOT.to_owned(), key])?; + store::copy_path(&base, &mut snapshot, &meta::path(&[key.as_str()]))?; } let mut base_heads: Vec<String> = params.base_heads.iter().map(|h| h.to_string()).collect(); @@ -422,7 +411,7 @@ mod tests { /// The user-visible view, without our own metadata. fn user_view(doc: &AutoCommit) -> JsonValue { let mut view = store::read_path(doc, &[]).unwrap().unwrap(); - view.as_object_mut().unwrap().remove(META_ROOT); + view.as_object_mut().unwrap().remove(meta::ROOT); view } @@ -580,19 +569,19 @@ mod tests { let mut a = doc(b"actor-a"); store::write_path( &mut a, - &[META_ROOT.into(), REMAKES.into(), "c-1".into()], + &meta::path(&[REMAKES, "c-1"]), &json!({ "state": "committed" }), ) .unwrap(); store::write_path( &mut a, - &[META_ROOT.into(), VOTES.into(), "pk".into()], + &meta::path(&[VOTES, "pk"]), &json!({ "remake": "c-1", "at": 1 }), ) .unwrap(); store::write_path( &mut a, - &[META_ROOT.into(), "forgotten".into(), "pk".into()], + &meta::path(&[meta::FORGOTTEN, "pk"]), &json!({ "at": 1 }), ) .unwrap(); @@ -602,7 +591,7 @@ mod tests { let policy = json!({ "replayWindow": { "epochs": 1 } }); let snap = build(&mut a, ¶ms(&heads, &policy)).unwrap(); - let meta = store::read_path(&snap.doc, &[META_ROOT.into()]) + let meta = store::read_path(&snap.doc, &meta::path(&[])) .unwrap() .unwrap(); assert!(meta.get(REMAKES).is_none()); diff --git a/src/remake/switch.rs b/src/remake/switch.rs index 9cc1ca0..65f296f 100644 --- a/src/remake/switch.rs +++ b/src/remake/switch.rs @@ -33,8 +33,9 @@ use super::kind::Translator; use super::local::LocalStatus; use super::quarantine; use super::records::{self, EpochRecord, Record}; -use super::snapshot::{self, META_ROOT, REMAKES}; +use super::snapshot; use super::vote; +use crate::document::meta::{self, REMAKES}; use crate::document::replica::Epoch; use crate::document::store; use crate::swarm::sync::paths_related; @@ -44,7 +45,7 @@ const TICK: Duration = Duration::from_secs(1); pub(crate) async fn run_worker<H: Host>(host: H) { info!("Starting remake worker"); - let records_dir = vec![META_ROOT.to_owned(), REMAKES.to_owned()]; + let records_dir = meta::path(&[REMAKES]); let mut changes = host.subscribe_changes(); let mut changes_closed = false; let mut tick = tokio::time::interval(TICK); diff --git a/src/swarm/bootstrap.rs b/src/swarm/bootstrap.rs index 5e9ac06..ad1de61 100644 --- a/src/swarm/bootstrap.rs +++ b/src/swarm/bootstrap.rs @@ -25,11 +25,12 @@ use tracing::{debug, info, warn}; use uuid::Uuid; use crate::document; +use crate::document::meta::{self, SWARM}; use crate::document::replica::Epoch; use crate::document::store; use crate::files::{FileKind, Files}; use crate::force::Force; -use crate::remake::snapshot::{self, META_ROOT, SWARM}; +use crate::remake::snapshot; use crate::state::AppState; use crate::swarm::peer::{remember_peer, send_hello_once}; @@ -139,7 +140,7 @@ pub(crate) async fn bootstrap(state: &AppState, seeds: &[String], start: Start) } fn swarm_id_path() -> Vec<String> { - vec![META_ROOT.to_owned(), SWARM.to_owned()] + meta::path(&[SWARM]) } /// The swarm ID the document names, if any. diff --git a/src/swarm/members.rs b/src/swarm/members.rs index 9b0de18..cba46f9 100644 --- a/src/swarm/members.rs +++ b/src/swarm/members.rs @@ -20,13 +20,12 @@ use automerge::AutoCommit; use serde_json::{Value as JsonValue, json}; use tracing::{debug, info, warn}; +use crate::document::meta; use crate::document::store::{self, StoreError}; use crate::state::AppState; -const META_ROOT: &str = "_continuum"; - pub(crate) fn members_dir() -> Vec<String> { - vec![META_ROOT.to_owned(), "members".to_owned()] + meta::path(&[meta::MEMBERS]) } pub(crate) fn member_path(pk: &str) -> Vec<String> { diff --git a/src/swarm/peer.rs b/src/swarm/peer.rs index 1240966..651e067 100644 --- a/src/swarm/peer.rs +++ b/src/swarm/peer.rs @@ -11,6 +11,7 @@ use tokio::sync::Mutex; use tokio::time::{Instant, MissedTickBehavior}; use tracing::{debug, info, instrument, trace, warn}; +use crate::document::meta; use crate::document::replica::{Epoch, EpochRelation}; use crate::document::store::{self, StoreError}; use crate::state::{AppState, HelloState, Liveness, PeerState, Standing}; @@ -20,13 +21,12 @@ use crate::transport::{self, ParsedMessage}; const PEER_TICK: Duration = Duration::from_millis(500); -const META_ROOT: &str = "_continuum"; const FORGET_CHECK_INTERVAL: Duration = Duration::from_secs(5); // --- forget records ---------------------------------------------------------- fn forgotten_dir() -> Vec<String> { - vec![META_ROOT.to_owned(), "forgotten".to_owned()] + meta::path(&[meta::FORGOTTEN]) } pub(crate) fn forgotten_path(pk: &str) -> Vec<String> { |
