use std::path::Path;
use serde_json::Value;
pub(crate) const PEER_PLANE_APPS: [&str; 4] = ["gossip", "iroh", "webv1", "friends"];
fn native_library_names(name: &str) -> [String; 2] {
let stem = format!("libnode_app_{}", name.replace('-', "_"));
[format!("{stem}.dylib"), format!("{stem}.so")]
}
pub(crate) fn missing_peer_plane_apps(modules_dir: &Path) -> Vec<&'static str> {
PEER_PLANE_APPS
.iter()
.copied()
.filter(|name| {
let dir = modules_dir.join(name);
let has_manifest = dir.join("manifest.json").is_file();
let has_library = native_library_names(name).iter().any(|lib| dir.join(lib).is_file());
!(has_manifest && has_library)
})
.collect()
}
fn app_answer(answer: &Value) -> &Value {
let result = answer.get("result").unwrap_or(answer);
result.get("data").filter(|d| d.is_object()).unwrap_or(result)
}
pub(crate) fn iroh_endpoint_id(answer: &Value) -> Option<String> {
app_answer(answer)
.get("endpoint_id")
.and_then(Value::as_str)
.filter(|id| !id.is_empty())
.map(str::to_string)
}
pub(crate) fn card_names_iroh_endpoint(card: &Value, endpoint_id: &str) -> bool {
card.get("iroh_endpoint_id").and_then(Value::as_str) == Some(endpoint_id)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Link {
pub node_id: String,
pub kind: String,
pub transport: String,
}
pub(crate) fn linked_peers(answer: &Value) -> Vec<Link> {
let text = |row: &Value, key: &str| row.get(key).and_then(Value::as_str).unwrap_or_default().to_string();
app_answer(answer)
.get("connections")
.and_then(Value::as_array)
.map(|rows| {
rows.iter()
.filter(|row| row.get("connected").and_then(Value::as_bool).unwrap_or(false))
.map(|row| Link {
node_id: text(row, "node_id"),
kind: text(row, "kind"),
transport: text(row, "transport"),
})
.filter(|link| !link.node_id.is_empty())
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn every_peer_plane_app_is_missing_from_an_empty_modules_dir() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(missing_peer_plane_apps(tmp.path()), PEER_PLANE_APPS.to_vec());
}
#[test]
fn an_app_needs_both_its_manifest_and_its_built_library() {
let tmp = tempfile::tempdir().unwrap();
let modules = tmp.path();
for name in ["gossip", "iroh", "webv1"] {
std::fs::create_dir_all(modules.join(name)).unwrap();
}
std::fs::write(modules.join("gossip/manifest.json"), "{}").unwrap();
std::fs::write(modules.join("gossip/libnode_app_gossip.dylib"), "").unwrap();
std::fs::write(modules.join("iroh/manifest.json"), "{}").unwrap();
std::fs::write(modules.join("webv1/libnode_app_webv1.so"), "").unwrap();
assert_eq!(missing_peer_plane_apps(modules), vec!["iroh", "webv1", "friends"]);
}
#[test]
fn the_endpoint_id_is_read_through_both_envelopes() {
let answer = json!({
"capability": "iroh.publish_endpoint",
"provider": "iroh",
"result": { "ok": true, "data": { "changed": false, "endpoint_id": "8dec61" } },
});
assert_eq!(iroh_endpoint_id(&answer).as_deref(), Some("8dec61"));
let unbound = json!({ "result": { "ok": true, "data": { "endpoint_id": "" } } });
assert_eq!(iroh_endpoint_id(&unbound), None);
let refused = json!({ "result": { "ok": false, "message": "iroh_endpoint_unavailable" } });
assert_eq!(iroh_endpoint_id(&refused), None);
}
#[test]
fn a_card_names_the_endpoint_only_when_the_ids_agree() {
let card = json!({ "node_id": "03aa", "iroh_endpoint_id": "8dec61" });
assert!(card_names_iroh_endpoint(&card, "8dec61"));
assert!(!card_names_iroh_endpoint(&card, "153372"));
assert!(!card_names_iroh_endpoint(&json!({ "node_id": "03aa" }), "8dec61"));
}
#[test]
fn linked_peers_lists_connected_rows_with_their_path() {
let answer = json!({
"capability": "core.network.list_connections",
"provider": "__core__",
"result": {
"connections": [
{
"address": "[2405:4803::1]:60610",
"carriers": { "iroh": true },
"connected": true,
"direction": "outbound",
"kind": "direct",
"node_id": "0306447",
"transport": "iroh"
},
{ "node_id": "02dead", "connected": false, "kind": "relayed", "transport": "iroh" }
],
"total": 2,
"unidentified_live_connections": 0
}
});
assert_eq!(
linked_peers(&answer),
vec![Link { node_id: "0306447".into(), kind: "direct".into(), transport: "iroh".into() }]
);
assert!(linked_peers(&json!({ "result": { "connections": [], "total": 0 } })).is_empty());
}
}