1use crate::cbor::Value;
5
6use super::payload::slot_field_names;
7use super::{
8 entry, id_field, malformed, text_field, unsigned, Record, RecordError, RecordType,
9 CLOCK_TOLERANCE_MS,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Reason {
15 Shutdown,
16 Moved,
17 Revoked,
18}
19
20impl Reason {
21 fn name(self) -> &'static str {
22 match self {
23 Reason::Shutdown => "shutdown",
24 Reason::Moved => "moved",
25 Reason::Revoked => "revoked",
26 }
27 }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct TombstoneOptions {
34 pub detail: String,
35 pub ttl_ms: u64,
36}
37
38pub fn new_tombstone(
43 withdrawn: &Record,
44 reason: Reason,
45 opts: &TombstoneOptions,
46) -> Result<Record, RecordError> {
47 if withdrawn.record_type == RecordType::TOMBSTONE {
48 return Err(RecordError::TombstoneOfATombstone);
49 }
50 let mut entries = vec![
51 entry(
52 "withdrawn_type",
53 Value::Int(i128::from(withdrawn.record_type.0)),
54 ),
55 entry(
56 "withdrawn_version",
57 Value::Bytes(withdrawn.version.to_vec()),
58 ),
59 entry("reason", Value::text(reason.name())),
60 ];
61 entries.extend(slot_fields(withdrawn)?);
62 if !opts.detail.is_empty() {
63 entries.push(entry("detail", Value::text(opts.detail.clone())));
64 }
65 let ttl_ms = if opts.ttl_ms == 0 {
66 CLOCK_TOLERANCE_MS
67 } else {
68 opts.ttl_ms
69 };
70 let mut r = unsigned(RecordType::TOMBSTONE, Value::Map(entries), ttl_ms);
71 r.expires_at = (r.created_at + ttl_ms).max(withdrawn.expires_at + CLOCK_TOLERANCE_MS);
72 Ok(r)
73}
74
75fn slot_fields(withdrawn: &Record) -> Result<Vec<(Value, Value)>, RecordError> {
76 if withdrawn.record_type >= RecordType::DOMAIN_MIN {
77 return Ok(withdrawn
78 .subject
79 .as_ref()
80 .map(|s| vec![entry("subject", Value::Bytes(s.clone()))])
81 .unwrap_or_default());
82 }
83 slot_field_names(i128::from(withdrawn.record_type.0))
84 .iter()
85 .map(|name| {
86 withdrawn
87 .payload
88 .get(name)
89 .map(|v| entry(name, v.clone()))
90 .ok_or_else(|| malformed(format!("the withdrawn record has no {name}")))
91 })
92 .collect()
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
98pub struct Tombstone {
99 pub withdrawn_type: RecordType,
100 pub withdrawn_version: [u8; 16],
101 pub reason: String,
102 pub detail: String,
103 pub realm_id: [u8; 32],
104 pub member_node: [u8; 32],
105 pub procedure: String,
106 pub param_name: String,
107 pub station_id: [u8; 32],
108 pub mcid: Vec<u8>,
109 pub org_name: String,
110 pub advertiser: [u8; 32],
111 pub subject: Vec<u8>,
112}
113
114pub fn read_tombstone(r: &Record) -> Result<Tombstone, RecordError> {
116 if r.record_type != RecordType::TOMBSTONE {
117 return Err(malformed("not a tombstone"));
118 }
119 let p = &r.payload;
120 let withdrawn = match p.get("withdrawn_type") {
121 Some(Value::Int(n)) if (1..=255).contains(n) => RecordType(*n as u8),
122 _ => {
123 return Err(malformed(
124 "a withdrawn_type that is not an integer from 1 to 255",
125 ))
126 }
127 };
128 let bytes = |name: &str| match p.get(name) {
129 Some(Value::Bytes(b)) => b.clone(),
130 _ => Vec::new(),
131 };
132 Ok(Tombstone {
133 withdrawn_type: withdrawn,
134 withdrawn_version: bytes("withdrawn_version").try_into().unwrap_or([0; 16]),
135 reason: text_field(p, "reason"),
136 detail: text_field(p, "detail"),
137 realm_id: id_field(p, "realm_id"),
138 member_node: id_field(p, "member_node"),
139 procedure: text_field(p, "procedure"),
140 param_name: text_field(p, "param_name"),
141 station_id: id_field(p, "station_id"),
142 mcid: bytes("mcid"),
143 org_name: text_field(p, "org_name"),
144 advertiser: id_field(p, "advertiser"),
145 subject: bytes("subject"),
146 })
147}