Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

sync/src/applied.rs

//! **The downstream's record**: per upstream, the newest generation applied and
//! every invalidation in force here.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::{Invalidation, SyncError, SyncManifest};

/// What one upstream's syncs did here.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppliedRecord {
    pub last_generation: u64,
    pub applied_at: i64,
    pub applies: u64,
    /// Every invalidation from this upstream in force here, by id.
    #[serde(default)]
    pub invalidations: BTreeMap<String, Invalidation>,
}

/// How a manifest stands against the record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Admission {
    /// A newer generation.
    New,
    /// The generation already applied, again (a second stick, a retried pull):
    /// applied again, which changes nothing, and acknowledged again.
    Replay,
}

/// **The downstream's record.** Kept as one JSON file by the server.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Applied {
    #[serde(default)]
    pub upstreams: BTreeMap<String, AppliedRecord>,
}

impl Applied {
    /// May `m` be applied? A generation below the one applied is a rollback.
    pub fn admit(&self, m: &SyncManifest) -> Result<Admission, SyncError> {
        let last = self
            .upstreams
            .get(&m.upstream)
            .map(|r| r.last_generation)
            .unwrap_or(0);
        if m.generation < last {
            return Err(SyncError::Rollback {
                upstream: m.upstream.clone(),
                applied: last,
                offered: m.generation,
            });
        }
        Ok(if m.generation == last && last > 0 {
            Admission::Replay
        } else {
            Admission::New
        })
    }

    /// Record that `m` was applied: its generation and its invalidations.
    pub fn record(&mut self, m: &SyncManifest, now: i64) {
        let r = self.upstreams.entry(m.upstream.clone()).or_default();
        r.last_generation = r.last_generation.max(m.generation);
        r.applied_at = now;
        r.applies += 1;
        for i in &m.invalidations {
            r.invalidations
                .entry(i.id.clone())
                .or_insert_with(|| i.clone());
        }
    }

    /// The ids of `upstream`'s invalidations in force here.
    pub fn ids(&self, upstream: &str) -> Vec<String> {
        self.upstreams
            .get(upstream)
            .map(|r| r.invalidations.keys().cloned().collect())
            .unwrap_or_default()
    }

    /// The invalidation in force here that names this package, from any upstream.
    pub fn blocking(&self, coordinate: &str, sha256: &str) -> Option<&Invalidation> {
        self.upstreams
            .values()
            .flat_map(|r| r.invalidations.values())
            .find(|i| i.matches(coordinate, sha256))
    }

    pub fn last_generation(&self, upstream: &str) -> u64 {
        self.upstreams
            .get(upstream)
            .map(|r| r.last_generation)
            .unwrap_or(0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Reason, MANIFEST_SCHEMA};

    fn m(generation: u64, invs: Vec<Invalidation>) -> SyncManifest {
        SyncManifest {
            schema: MANIFEST_SCHEMA.into(),
            upstream: "dmz".into(),
            generation,
            issued_at: 0,
            carrier: "usb".into(),
            added: vec![],
            invalidations: invs,
        }
    }

    #[test]
    fn a_rollback_is_refused_and_a_replay_is_admitted() {
        let mut a = Applied::default();
        assert_eq!(a.admit(&m(3, vec![])).unwrap(), Admission::New);
        a.record(&m(3, vec![]), 1);
        assert_eq!(a.admit(&m(3, vec![])).unwrap(), Admission::Replay);
        assert_eq!(a.admit(&m(4, vec![])).unwrap(), Admission::New);
        let e = a.admit(&m(2, vec![])).unwrap_err();
        assert_eq!(e.number(), 4, "{e}");
    }

    #[test]
    fn invalidations_stay_in_force_and_block_by_bytes() {
        let mut a = Applied::default();
        let i = Invalidation::new("x@1", Some("ab".into()), Reason::Malware, "", 2, 0, "");
        a.record(&m(2, vec![i.clone()]), 1);
        a.record(&m(3, vec![i.clone()]), 2);
        assert_eq!(a.ids("dmz"), vec![i.id.clone()]);
        assert_eq!(a.blocking("other-name", "AB"), Some(&i));
        assert_eq!(a.blocking("x@1", "cd"), None);
    }
}