bear_cli/cloudkit/
vector_clock.rs1use std::collections::BTreeMap;
2use std::io::Cursor;
3use std::process::Command;
4
5use anyhow::{Result, bail};
6use base64::Engine as _;
7use base64::engine::general_purpose::STANDARD as BASE64;
8use plist::Value;
9use uuid::Uuid;
10
11pub fn decode(encoded: &str) -> Result<BTreeMap<String, u64>> {
13 let bytes = BASE64.decode(encoded)?;
14 let value = plist::from_reader(Cursor::new(&bytes))?;
15 parse_dict(value)
16}
17
18fn parse_dict(value: Value) -> Result<BTreeMap<String, u64>> {
19 match value {
20 Value::Dictionary(dict) => {
21 let mut map = BTreeMap::new();
22 for (key, val) in dict {
23 let n = match val {
24 Value::Integer(i) => i.as_unsigned().unwrap_or(0),
25 _ => bail!("vector clock value is not an integer"),
26 };
27 map.insert(key, n);
28 }
29 Ok(map)
30 }
31 _ => bail!("vector clock is not a plist dictionary"),
32 }
33}
34
35pub fn encode(clock: &BTreeMap<String, u64>) -> Result<String> {
37 let dict: plist::Dictionary = clock
38 .iter()
39 .map(|(k, v)| (k.clone(), Value::Integer((*v).into())))
40 .collect();
41 let mut buf = Vec::new();
42 Value::Dictionary(dict).to_writer_binary(&mut buf)?;
43 Ok(BASE64.encode(&buf))
44}
45
46pub fn local_device_id() -> String {
49 if let Ok(output) = Command::new("ioreg")
50 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
51 .output()
52 {
53 if output.status.success() {
54 let stdout = String::from_utf8_lossy(&output.stdout);
55 for line in stdout.lines() {
56 if let Some((_, rest)) = line.split_once("\"IOPlatformUUID\" = \"") {
57 if let Some((uuid, _)) = rest.split_once('"') {
58 let uuid = uuid.trim();
59 if !uuid.is_empty() {
60 return uuid.to_uppercase();
61 }
62 }
63 }
64 }
65 }
66 }
67
68 Uuid::new_v4().to_string().to_uppercase()
69}
70
71pub fn increment(existing: Option<&str>, device: &str) -> Result<String> {
74 let mut clock = match existing {
75 Some(enc) if !enc.is_empty() => decode(enc)?,
76 _ => BTreeMap::new(),
77 };
78 let max = clock.values().max().copied().unwrap_or(0);
79 clock.insert(device.to_string(), max + 1);
80 encode(&clock)
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn roundtrip_empty() {
89 let enc = encode(&BTreeMap::new()).unwrap();
90 let dec = decode(&enc).unwrap();
91 assert!(dec.is_empty());
92 }
93
94 #[test]
95 fn increment_new_device() {
96 let enc = increment(None, "94536980-D452-5A88-9C1C-4A4022CFD280").unwrap();
97 let clock = decode(&enc).unwrap();
98 assert_eq!(clock["94536980-D452-5A88-9C1C-4A4022CFD280"], 1);
99 }
100
101 #[test]
102 fn increment_preserves_existing() {
103 let initial = {
104 let mut m = BTreeMap::new();
105 m.insert("iPhone".to_string(), 5u64);
106 m.insert("Mac".to_string(), 3u64);
107 encode(&m).unwrap()
108 };
109 let enc = increment(Some(&initial), "Bear CLI").unwrap();
110 let clock = decode(&enc).unwrap();
111 assert_eq!(clock["iPhone"], 5);
112 assert_eq!(clock["Mac"], 3);
113 assert_eq!(clock["Bear CLI"], 6); }
115}