bear_cli/cloudkit/
vector_clock.rs1use std::collections::BTreeMap;
2use std::io::Cursor;
3
4use anyhow::{Result, bail};
5use base64::Engine as _;
6use base64::engine::general_purpose::STANDARD as BASE64;
7use plist::Value;
8
9pub fn decode(encoded: &str) -> Result<BTreeMap<String, u64>> {
11 let bytes = BASE64.decode(encoded)?;
12 let value = plist::from_reader(Cursor::new(&bytes))?;
13 parse_dict(value)
14}
15
16fn parse_dict(value: Value) -> Result<BTreeMap<String, u64>> {
17 match value {
18 Value::Dictionary(dict) => {
19 let mut map = BTreeMap::new();
20 for (key, val) in dict {
21 let n = match val {
22 Value::Integer(i) => i.as_unsigned().unwrap_or(0),
23 _ => bail!("vector clock value is not an integer"),
24 };
25 map.insert(key, n);
26 }
27 Ok(map)
28 }
29 _ => bail!("vector clock is not a plist dictionary"),
30 }
31}
32
33pub fn encode(clock: &BTreeMap<String, u64>) -> Result<String> {
35 let dict: plist::Dictionary = clock
36 .iter()
37 .map(|(k, v)| (k.clone(), Value::Integer((*v).into())))
38 .collect();
39 let mut buf = Vec::new();
40 Value::Dictionary(dict).to_writer_binary(&mut buf)?;
41 Ok(BASE64.encode(&buf))
42}
43
44pub fn increment(existing: Option<&str>, device: &str) -> Result<String> {
47 let mut clock = match existing {
48 Some(enc) if !enc.is_empty() => decode(enc)?,
49 _ => BTreeMap::new(),
50 };
51 let max = clock.values().max().copied().unwrap_or(0);
52 clock.insert(device.to_string(), max + 1);
53 encode(&clock)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn roundtrip_empty() {
62 let enc = encode(&BTreeMap::new()).unwrap();
63 let dec = decode(&enc).unwrap();
64 assert!(dec.is_empty());
65 }
66
67 #[test]
68 fn increment_new_device() {
69 let enc = increment(None, "Bear CLI").unwrap();
70 let clock = decode(&enc).unwrap();
71 assert_eq!(clock["Bear CLI"], 1);
72 }
73
74 #[test]
75 fn increment_preserves_existing() {
76 let initial = {
77 let mut m = BTreeMap::new();
78 m.insert("iPhone".to_string(), 5u64);
79 m.insert("Mac".to_string(), 3u64);
80 encode(&m).unwrap()
81 };
82 let enc = increment(Some(&initial), "Bear CLI").unwrap();
83 let clock = decode(&enc).unwrap();
84 assert_eq!(clock["iPhone"], 5);
85 assert_eq!(clock["Mac"], 3);
86 assert_eq!(clock["Bear CLI"], 6); }
88}