use std::io;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::identity::{IdentityError, PubKey};
use crate::peer_meta::PeerMeta;
#[derive(Debug, Error)]
pub enum TrustError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid peer ID: {0}")]
InvalidPeerId(#[from] IdentityError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustedPeer {
pub name: String,
pub pubkey: PubKey,
pub addr: String,
pub meta: PeerMeta,
}
impl Serialize for TrustedPeer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("TrustedPeer", 4)?;
s.serialize_field("name", &self.name)?;
s.serialize_field("pubkey", &self.pubkey.to_peer_id())?;
s.serialize_field("addr", &self.addr)?;
s.serialize_field("meta", &self.meta)?;
s.end()
}
}
impl<'de> Deserialize<'de> for TrustedPeer {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct TrustedPeerHelper {
name: String,
pubkey: String,
addr: String,
#[serde(default)]
meta: PeerMeta,
}
let helper = TrustedPeerHelper::deserialize(deserializer)?;
let pubkey = PubKey::from_peer_id(&helper.pubkey).map_err(serde::de::Error::custom)?;
Ok(TrustedPeer {
name: helper.name,
pubkey,
addr: helper.addr,
meta: helper.meta,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustedPeers {
pub peers: Vec<TrustedPeer>,
}
impl TrustedPeers {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.peers.is_empty()
}
pub fn has_peers(&self) -> bool {
!self.peers.is_empty()
}
pub fn len(&self) -> usize {
self.peers.len()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_or_default(path: &Path) -> Result<Self, TrustError> {
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(path)?;
let peers = serde_json::from_str(&content)?;
Ok(peers)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn load(path: &Path) -> Result<Self, TrustError> {
let content = tokio::fs::read_to_string(path).await?;
let peers = serde_json::from_str(&content)?;
Ok(peers)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn save(&self, path: &Path) -> Result<(), TrustError> {
let content = serde_json::to_string_pretty(self)?;
tokio::fs::write(path, content).await?;
Ok(())
}
pub fn is_trusted(&self, pubkey: &PubKey) -> bool {
self.peers.iter().any(|p| &p.pubkey == pubkey)
}
pub fn get_peer(&self, pubkey: &PubKey) -> Option<&TrustedPeer> {
self.peers.iter().find(|p| &p.pubkey == pubkey)
}
pub fn get_by_name(&self, name: &str) -> Option<&TrustedPeer> {
self.peers.iter().find(|p| p.name == name)
}
pub fn upsert(&mut self, peer: TrustedPeer) {
if let Some(existing) = self.peers.iter_mut().find(|p| p.pubkey == peer.pubkey) {
*existing = peer;
} else {
self.peers.push(peer);
}
}
pub fn remove(&mut self, pubkey: &PubKey) -> bool {
let len_before = self.peers.len();
self.peers.retain(|p| &p.pubkey != pubkey);
self.peers.len() != len_before
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_trusted_peer_fields() {
let peer = TrustedPeer {
name: "test-peer".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "tcp://127.0.0.1:4200".to_string(),
meta: PeerMeta::default(),
};
assert_eq!(peer.name, "test-peer");
assert_eq!(peer.pubkey.as_bytes()[0], 42);
assert_eq!(peer.addr, "tcp://127.0.0.1:4200");
}
#[test]
fn test_trusted_peers_fields() {
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "uds:///tmp/test.sock".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "peer1");
}
#[test]
fn test_trusted_peer_json_roundtrip() {
let peer = TrustedPeer {
name: "coding-meerkat".to_string(),
pubkey: PubKey::new([7u8; 32]),
addr: "uds:///tmp/meerkat-coding.sock".to_string(),
meta: crate::PeerMeta::default(),
};
let json = serde_json::to_string(&peer).unwrap();
let decoded: TrustedPeer = serde_json::from_str(&json).unwrap();
assert_eq!(peer, decoded);
}
#[test]
fn test_trusted_peers_json_roundtrip() {
let peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://192.168.1.50:4200".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "uds:///tmp/peer2.sock".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let json = serde_json::to_string_pretty(&peers).unwrap();
let decoded: TrustedPeers = serde_json::from_str(&json).unwrap();
assert_eq!(peers, decoded);
}
#[tokio::test]
async fn test_trusted_peers_load() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("trusted_peers.json");
let json = r#"{
"peers": [
{ "name": "coding-meerkat", "pubkey": "ed25519:KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio=", "addr": "uds:///tmp/meerkat-coding.sock" }
]
}"#;
tokio::fs::write(&path, json).await.unwrap();
let peers = TrustedPeers::load(&path).await.unwrap();
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "coding-meerkat");
assert_eq!(peers.peers[0].pubkey, PubKey::new([42u8; 32]));
}
#[tokio::test]
async fn test_trusted_peers_save() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("trusted_peers.json");
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "test-peer".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
peers.save(&path).await.unwrap();
assert!(path.exists());
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert!(content.contains("test-peer"));
assert!(content.contains("ed25519:"));
}
#[test]
fn test_is_trusted_found() {
let pubkey = PubKey::new([42u8; 32]);
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "trusted".to_string(),
pubkey,
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(peers.is_trusted(&pubkey));
}
#[test]
fn test_is_trusted_not_found() {
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "trusted".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let unknown = PubKey::new([99u8; 32]);
assert!(!peers.is_trusted(&unknown));
}
#[test]
fn test_get_peer() {
let pubkey = PubKey::new([42u8; 32]);
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "the-peer".to_string(),
pubkey,
addr: "tcp://localhost:4200".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let found = peers.get_peer(&pubkey);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "the-peer");
let unknown = PubKey::new([99u8; 32]);
assert!(peers.get_peer(&unknown).is_none());
}
#[test]
fn test_get_by_name() {
let peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "alpha".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "beta".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let found = peers.get_by_name("beta");
assert!(found.is_some());
assert_eq!(found.unwrap().pubkey, PubKey::new([2u8; 32]));
assert!(peers.get_by_name("gamma").is_none());
}
#[test]
fn test_json_format_matches_spec() {
let peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "coding-meerkat".to_string(),
pubkey: PubKey::new([7u8; 32]),
addr: "uds:///tmp/meerkat-coding.sock".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let json = serde_json::to_string_pretty(&peers).unwrap();
assert!(json.contains("\"peers\""));
assert!(json.contains("\"name\""));
assert!(json.contains("\"pubkey\""));
assert!(json.contains("\"addr\""));
assert!(json.contains("ed25519:"));
assert!(json.contains("coding-meerkat"));
assert!(json.contains("uds:///tmp/meerkat-coding.sock"));
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let pubkey_str = parsed["peers"][0]["pubkey"].as_str().unwrap();
assert!(pubkey_str.starts_with("ed25519:"));
}
#[tokio::test]
async fn test_trusted_peers_persistence_roundtrip() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("trusted_peers.json");
let original = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://192.168.1.50:4200".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "uds:///tmp/peer2.sock".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
original.save(&path).await.unwrap();
let loaded = TrustedPeers::load(&path).await.unwrap();
assert_eq!(original, loaded);
}
#[test]
fn test_upsert_adds_new_peer() {
let mut peers = TrustedPeers::new();
assert_eq!(peers.peers.len(), 0);
let peer = TrustedPeer {
name: "new-peer".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "uds:///tmp/new.sock".to_string(),
meta: crate::PeerMeta::default(),
};
peers.upsert(peer);
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "new-peer");
}
#[test]
fn test_upsert_updates_existing_peer() {
let mut peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "original".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "uds:///tmp/original.sock".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let updated = TrustedPeer {
name: "updated".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "uds:///tmp/updated.sock".to_string(),
meta: crate::PeerMeta::default(),
};
peers.upsert(updated);
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "updated");
assert_eq!(peers.peers[0].addr, "uds:///tmp/updated.sock");
}
#[test]
fn test_remove_existing_peer() {
let mut peers = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
let removed = peers.remove(&PubKey::new([1u8; 32]));
assert!(removed);
assert_eq!(peers.peers.len(), 1);
assert_eq!(peers.peers[0].name, "peer2");
}
#[test]
fn test_is_empty() {
let peers = TrustedPeers::new();
assert!(peers.is_empty());
let peers_with_one = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(!peers_with_one.is_empty());
}
#[test]
fn test_has_peers() {
let peers = TrustedPeers::new();
assert!(!peers.has_peers());
let peers_with_one = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
}],
};
assert!(peers_with_one.has_peers());
}
#[test]
fn test_len() {
let peers = TrustedPeers::new();
assert_eq!(peers.len(), 0);
let peers_with_two = TrustedPeers {
peers: vec![
TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
},
TrustedPeer {
name: "peer2".to_string(),
pubkey: PubKey::new([2u8; 32]),
addr: "tcp://localhost:4202".to_string(),
meta: crate::PeerMeta::default(),
},
],
};
assert_eq!(peers_with_two.len(), 2);
}
#[test]
fn test_remove_nonexistent_peer() {
let mut peers = TrustedPeers {
peers: vec![TrustedPeer {
name: "peer1".to_string(),
pubkey: PubKey::new([1u8; 32]),
addr: "tcp://localhost:4201".to_string(),
meta: crate::PeerMeta::default(),
}],
};
let removed = peers.remove(&PubKey::new([99u8; 32]));
assert!(!removed);
assert_eq!(peers.peers.len(), 1);
}
#[test]
fn test_trusted_peer_with_meta() {
let meta = PeerMeta::default()
.with_description("Reviews code")
.with_label("lang", "rust");
let peer = TrustedPeer {
name: "reviewer".to_string(),
pubkey: PubKey::new([42u8; 32]),
addr: "inproc://reviewer".to_string(),
meta: meta.clone(),
};
let json = serde_json::to_string(&peer).unwrap();
let decoded: TrustedPeer = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.meta, meta);
assert_eq!(decoded.meta.description.as_deref(), Some("Reviews code"));
assert_eq!(
decoded.meta.labels.get("lang").map(String::as_str),
Some("rust")
);
}
#[test]
fn test_trusted_peer_without_meta_backward_compat() {
let json = r#"{
"name": "legacy-peer",
"pubkey": "ed25519:KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio=",
"addr": "tcp://127.0.0.1:4200"
}"#;
let peer: TrustedPeer = serde_json::from_str(json).unwrap();
assert_eq!(peer.name, "legacy-peer");
assert_eq!(peer.meta, PeerMeta::default());
}
}