use crate::cbor::Value;
use crate::record::{self, RecordType, Verified};
use crate::profile::Profile;
use super::{now_ms, Call, Link, LinkError};
const DHT_V: i128 = 1;
const MAX_REASON_BYTES: usize = 64;
#[derive(Debug, Clone, PartialEq)]
enum DhtReply {
Stored,
Refused(String),
Record(Vec<u8>),
NotFound,
Records(Vec<Vec<u8>>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReplyRefusal {
VersionUnsupported,
Malformed,
}
impl From<ReplyRefusal> for LinkError {
fn from(refusal: ReplyRefusal) -> LinkError {
match refusal {
ReplyRefusal::VersionUnsupported => LinkError::ReplyVersionUnsupported,
ReplyRefusal::Malformed => LinkError::UnexpectedReply("malformed_reply".into()),
}
}
}
fn read_reply(procedure: &str, reply: &Value) -> Result<DhtReply, ReplyRefusal> {
let Value::Map(pairs) = reply else {
return Err(ReplyRefusal::Malformed);
};
match reply.get("v") {
Some(Value::Int(DHT_V)) => {}
Some(Value::Int(_)) => return Err(ReplyRefusal::VersionUnsupported),
_ => return Err(ReplyRefusal::Malformed),
}
let result = match reply.get("result") {
Some(Value::Text(r)) => r.as_str(),
_ => return Err(ReplyRefusal::Malformed),
};
let read = match (procedure, result) {
("_dht.put_record", "ok") => Some((2, DhtReply::Stored)),
("_dht.put_record", "refused") => match reply.get("reason") {
Some(Value::Text(r)) if r.len() <= MAX_REASON_BYTES => {
Some((3, DhtReply::Refused(r.clone())))
}
_ => None,
},
("_dht.find_record", "record") => match reply.get("record") {
Some(Value::Bytes(wire)) => Some((3, DhtReply::Record(wire.clone()))),
_ => None,
},
("_dht.find_record", "not_found") => Some((2, DhtReply::NotFound)),
("_dht.find_records" | "_dht.find_records_by_type", "records") => {
record_list(reply.get("records")).map(|records| (3, DhtReply::Records(records)))
}
_ => None,
};
match read {
Some((size, reply)) if pairs.len() == size => Ok(reply),
_ => Err(ReplyRefusal::Malformed),
}
}
fn record_list(v: Option<&Value>) -> Option<Vec<Vec<u8>>> {
let Some(Value::List(items)) = v else {
return None;
};
items
.iter()
.map(|item| match item {
Value::Bytes(wire) => Some(wire.clone()),
_ => None,
})
.collect()
}
fn dht_args(name: &str, value: Value) -> Value {
Value::Map(vec![
(Value::text("v"), Value::Int(DHT_V)),
(Value::text(name), value),
])
}
impl Link {
pub async fn put_record(&self, wire: &[u8]) -> Result<(), LinkError> {
let args = dht_args("record", Value::Bytes(wire.to_vec()));
match self.dht_call("_dht.put_record", args).await? {
DhtReply::Stored => Ok(()),
DhtReply::Refused(reason) => Err(LinkError::RecordRefused(reason)),
_ => Err(ReplyRefusal::Malformed.into()),
}
}
pub async fn find_record(&self, key: &[u8; 32]) -> Result<Verified, LinkError> {
let args = dht_args("key", Value::Bytes(key.to_vec()));
match self.dht_call("_dht.find_record", args).await? {
DhtReply::NotFound => Err(LinkError::RecordNotFound),
DhtReply::Record(wire) => Ok(record::verify(&wire, self.profile(), now_ms())?),
_ => Err(ReplyRefusal::Malformed.into()),
}
}
pub async fn find_records(&self, key: &[u8; 32]) -> Result<(Vec<Verified>, usize), LinkError> {
let args = dht_args("key", Value::Bytes(key.to_vec()));
self.verified_list("_dht.find_records", args).await
}
pub async fn find_records_by_type(
&self,
t: RecordType,
) -> Result<(Vec<Verified>, usize), LinkError> {
let args = dht_args("type", Value::Int(i128::from(t.0)));
self.verified_list("_dht.find_records_by_type", args).await
}
async fn verified_list(
&self,
procedure: &str,
args: Value,
) -> Result<(Vec<Verified>, usize), LinkError> {
let DhtReply::Records(records) = self.dht_call(procedure, args).await? else {
return Err(ReplyRefusal::Malformed.into());
};
Ok(verified_records(&records, self.profile(), now_ms()))
}
async fn dht_call(&self, procedure: &str, args: Value) -> Result<DhtReply, LinkError> {
let reply = self
.call(Call {
procedure: procedure.to_string(),
payload: args,
..Call::default()
})
.await?;
Ok(read_reply(procedure, &reply)?)
}
}
fn verified_records(records: &[Vec<u8>], profile: Profile, now: i64) -> (Vec<Verified>, usize) {
let verified: Vec<Verified> = records
.iter()
.filter_map(|wire| record::verify(wire, profile, now).ok())
.collect();
let dropped = records.len() - verified.len();
(verified, dropped)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cbor;
use crate::wire_vectors::{self, Case};
fn sent_args(c: &Case, args: &Value) -> Value {
match c.text("procedure") {
"_dht.put_record" => dht_args("record", args.get("record").unwrap().clone()),
"_dht.find_records_by_type" => dht_args("type", args.get("type").unwrap().clone()),
_ => dht_args("key", args.get("key").unwrap().clone()),
}
}
fn reply_verdict(c: &Case) -> (String, String, Option<usize>) {
let reply = cbor::decode(&c.hex("reply")).unwrap();
let profile = Profile::parse("pq_pure").unwrap();
match read_reply(c.text("procedure"), &reply) {
Err(ReplyRefusal::VersionUnsupported) => {
("refuse".into(), "reply_version_unsupported".into(), None)
}
Err(ReplyRefusal::Malformed) => ("refuse".into(), "malformed_reply".into(), None),
Ok(DhtReply::Stored) => ("accept".into(), "ok".into(), None),
Ok(DhtReply::Refused(_)) => ("accept".into(), "refused".into(), None),
Ok(DhtReply::Record(_)) => ("accept".into(), "record".into(), None),
Ok(DhtReply::NotFound) => ("accept".into(), "not_found".into(), None),
Ok(DhtReply::Records(records)) => {
let kept = verified_records(&records, profile, c.int("now_ms").unwrap())
.0
.len();
("accept".into(), "records".into(), Some(kept))
}
}
}
#[test]
fn the_wire_1_0_dht_vectors() {
let file = wire_vectors::load("dht_procedures");
let accepted_args = file
.cases
.iter()
.filter(|c| c.has("args") && c.outcome() == "accept");
for c in accepted_args {
let args = cbor::decode(&c.hex("args")).unwrap();
assert_eq!(
cbor::encode(&sent_args(c, &args)).unwrap(),
c.hex("args"),
"{}",
c.id
);
}
for c in file.cases.iter().filter(|c| c.has("reply")) {
let (outcome, said, kept) = reply_verdict(c);
let want = match c.outcome() {
"accept" => c.expect_text("result"),
_ => c.reason(),
};
assert_eq!(
(outcome.as_str(), said.as_str()),
(c.outcome(), want),
"{}",
c.id
);
let want_kept = c.expect_int("records_kept").map(|n| n as usize);
assert!(
want_kept.is_none() || kept == want_kept,
"{}: records kept",
c.id
);
}
}
}