use super::Dht;
use super::bencode::{Dict, bytes, entry};
use super::krpc::{self, Message, Node, NodeId};
use super::round::Pending;
use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr;
#[derive(Debug, Default)]
pub(crate) struct Outcome {
pub(crate) replies: Vec<(Node, Dict)>,
pub(crate) errors: Vec<String>,
}
impl Dht {
pub(crate) fn search(&mut self, target: NodeId, q: &str) -> Result<Outcome, String> {
if self.bootstrap.is_empty() {
return Err("no bootstrap node to ask".into());
}
let mut asked: BTreeSet<SocketAddr> = BTreeSet::new();
let mut replied: BTreeSet<SocketAddr> = BTreeSet::new();
let mut pool: BTreeMap<([u8; 20], SocketAddr), Node> = BTreeMap::new();
let mut out = Outcome::default();
let mut claims = Vec::new();
let mut sent = 0usize;
let args = Dict::from([entry("target", bytes(&target.0))]);
let mut picks = self.bootstrap.clone();
let mut seeding = true;
loop {
let verb = if seeding { "find_node" } else { q };
let quota = if seeding {
picks.len()
} else {
self.config.alpha
};
let mut pending = Pending::new();
for addr in picks {
if pending.len() >= quota {
break;
}
asked.insert(addr);
self.ask(&mut pending, addr, verb, args.clone());
}
sent += pending.len();
self.collect(&mut pending, &mut |addr, message| match message {
Message::Reply { r, ip, .. } => {
let Some(id) = r
.get(b"id".as_slice())
.and_then(|v| v.as_bytes())
.and_then(NodeId::parse)
else {
return;
};
let node = Node { id, addr };
claims.extend(ip);
let this = (!seeding).then_some(node);
for near in krpc::nodes_of(&r).into_iter().chain(this) {
pool.insert((near.id.distance(&target), near.addr), near);
}
if !seeding {
replied.insert(addr);
out.replies.push((node, r));
}
}
Message::Error { code, message, .. } if !seeding => {
out.errors.push(format!("{addr}: {code} {message}"));
}
Message::Error { .. } => {}
})?;
picks = pool
.values()
.filter(|n| replied.contains(&n.addr) || !asked.contains(&n.addr))
.take(self.config.k)
.filter(|n| !asked.contains(&n.addr))
.map(|n| n.addr)
.collect();
seeding = picks.is_empty() && !pool.is_empty() && replied.len() < self.config.k;
if seeding {
picks.clone_from(&self.bootstrap);
}
if picks.is_empty() || sent >= self.config.max_queries {
break;
}
}
self.claims = claims;
if out.replies.is_empty() && out.errors.is_empty() {
return Err(format!("no DHT node answered {q} for {target}"));
}
out.replies.sort_by_key(|(n, _)| n.id.distance(&target));
Ok(out)
}
}