1use deaddrop_core::config::Config;
2use deaddrop_core::protocol::Message;
3use deaddrop_core::store::Store;
4use deaddrop_core::{PROTOCOL_LABEL, Result};
5use std::path::Path;
6
7pub mod daemon_service;
8
9#[derive(Debug, Clone)]
10pub struct Check {
11 pub ok: bool,
12 pub warn: bool,
13 pub name: String,
14 pub detail: String,
15}
16
17pub fn doctor(data_dir: &Path, cfg: &Config) -> Result<Vec<Check>> {
18 let mut out = Vec::new();
19 let id_path = data_dir.join("identities").join("identity.json");
20 if id_path.exists() {
21 match Store::open(data_dir, Default::default()).and_then(|s| s.load_identity()) {
22 Ok(id) => out.push(ok("identity", &id.peer_id.to_string())),
23 Err(e) => out.push(fail("identity", &e.to_string())),
24 }
25 } else {
26 out.push(fail("identity", "run `dd init`"));
27 }
28 out.push(ok("keystore", "identity.json (OS file permissions apply)"));
29 let probe = data_dir.join(".write-probe");
30 match std::fs::write(&probe, b"ok") {
31 Ok(()) => {
32 let _ = std::fs::remove_file(&probe);
33 out.push(ok("permissions", &data_dir.display().to_string()));
34 }
35 Err(e) => out.push(fail("permissions", &e.to_string())),
36 }
37 match Store::open(data_dir, Default::default()) {
38 Ok(s) => match s.verify() {
39 Ok(p) if p.is_empty() => out.push(ok("database", "sqlite + chunks")),
40 Ok(p) => out.push(warn(
41 "database",
42 &format!("{} issues (Drop bodies kept)", p.len()),
43 )),
44 Err(e) => out.push(fail("database", &e.to_string())),
45 },
46 Err(e) => out.push(fail("database", &e.to_string())),
47 }
48 out.push(ok(
49 "crypto backend",
50 "DefaultProvider (Ed25519/X25519/ChaCha20-Poly1305/BLAKE3)",
51 ));
52 if let Ok(s) = Store::open(data_dir, Default::default()) {
53 match s.stats() {
54 Ok(st) => {
55 let cap = deaddrop_core::config::Config::parse_bytes(&cfg.storage.maximum);
56 if st.physical_size + 64 * 1024 * 1024 > cap && cap > 0 {
57 out.push(warn(
58 "disk capacity",
59 &format!(
60 "{} used of {}",
61 deaddrop_core::store::format_bytes(st.physical_size),
62 cfg.storage.maximum
63 ),
64 ));
65 } else {
66 out.push(ok(
67 "disk capacity",
68 &deaddrop_core::store::format_bytes(st.physical_size),
69 ));
70 }
71 let now = deaddrop_core::store::unix_now();
72 let stale = s
73 .all_encounters()
74 .unwrap_or_default()
75 .into_iter()
76 .filter(|(_, e)| now.saturating_sub(e.last_seen) > 30 * 86_400)
77 .count();
78 if stale > 0 {
79 out.push(warn("stale peers", &format!("{stale} not seen in 30d")));
80 } else {
81 out.push(ok("stale peers", "none"));
82 }
83 match s.verify() {
84 Ok(p) if p.is_empty() => out.push(ok("Drop store", "chunk hashes match")),
85 Ok(p) => out.push(warn("corrupted Drops", &p.join("; "))),
86 Err(e) => out.push(fail("Drop store", &e.to_string())),
87 }
88 }
89 Err(e) => out.push(fail("storage stats", &e.to_string())),
90 }
91 }
92 out.push(if cfg.discovery.lan && cfg.discovery.mode.advertise_lan() {
93 ok(
94 "LAN discovery",
95 &format!("UDP {} ({:?})", cfg.discovery.lan_port, cfg.discovery.mode),
96 )
97 } else {
98 warn("LAN discovery", "disabled or hidden")
99 });
100 out.push(if cfg.transport.quic {
101 warn(
102 "QUIC",
103 "enabled in config; default `dd serve` listens TCP. QUIC types are EXPERIMENTAL",
104 )
105 } else {
106 warn("QUIC", "disabled in config")
107 });
108 out.push(warn(
109 "Bluetooth",
110 "unavailable (no BLE transport in this tree)",
111 ));
112 out.push(ok("routing engine", &cfg.routing.strategy));
113 let now = deaddrop_core::store::unix_now();
114 if now < 1_700_000_000 {
115 out.push(warn(
116 "time synchronization",
117 "system clock looks before 2023; sealed-until policy may mis-fire",
118 ));
119 } else {
120 out.push(ok(
121 "time synchronization",
122 "local clock present (no NTP client)",
123 ));
124 }
125 match deaddrop_net::control::read_info(data_dir) {
126 Ok(Some(info)) => match std::net::TcpStream::connect_timeout(
127 &info
128 .listen
129 .parse()
130 .unwrap_or_else(|_| "127.0.0.1:1".parse().expect("literal")),
131 std::time::Duration::from_millis(200),
132 ) {
133 Ok(_) => out.push(ok(
134 "daemon",
135 &format!("pid {} control {}", info.pid, info.listen),
136 )),
137 Err(_) => out.push(warn(
138 "daemon",
139 "control.json present but not reachable — run `dd daemon start`",
140 )),
141 },
142 _ => out.push(warn("daemon", "not running (CLI store still works)")),
143 }
144 out.push(ok("protocol compatibility", PROTOCOL_LABEL));
145 out.push(ok(
146 "network interfaces",
147 "OS default; LAN uses UDP broadcast",
148 ));
149 Ok(out)
150}
151
152pub fn doctor_repair(data_dir: &Path) -> Result<Vec<String>> {
153 let store = Store::open(data_dir, Default::default())?;
154 store.repair_safe()
155}
156
157pub fn inspect_protocol_bytes(bytes: &[u8]) -> String {
158 let payload = if bytes.len() >= 4 {
159 let n = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
160 if n + 4 == bytes.len() {
161 &bytes[4..]
162 } else {
163 bytes
164 }
165 } else {
166 bytes
167 };
168 match deaddrop_core::protocol::decode_cbor::<Message>(payload) {
169 Ok(m) => format!(
170 "Frame Type {}\nVersion {PROTOCOL_LABEL}\nPayload Size {} bytes\nAuthenticated (session layer; inspect is offline)\n",
171 m.name(),
172 payload.len()
173 ),
174 Err(e) => {
175 format!("decode error: {e}\n(not a DDP Message; JSON/debug export is separate)\n")
176 }
177 }
178}
179
180pub fn format_uptime(secs: u64) -> String {
181 let h = secs / 3600;
182 let m = (secs % 3600) / 60;
183 let s = secs % 60;
184 format!("{h:02}:{m:02}:{s:02}")
185}
186
187pub fn parse_duration_or_rfc3339(s: &str) -> Option<u64> {
188 let t = s.trim();
189 if let Some(d) = t.strip_suffix('d') {
190 return d.parse::<u64>().ok().map(|n| n * 86400);
191 }
192 if let Some(h) = t.strip_suffix('h') {
193 return h.parse::<u64>().ok().map(|n| n * 3600);
194 }
195 if let Some(m) = t.strip_suffix('m') {
196 return m.parse::<u64>().ok().map(|n| n * 60);
197 }
198 if let Some(sec) = t.strip_suffix('s') {
199 return sec.parse::<u64>().ok();
200 }
201 if t.contains('T') {
202 return parse_rfc3339_utc(t);
203 }
204 t.parse::<u64>().ok()
205}
206
207fn parse_rfc3339_utc(s: &str) -> Option<u64> {
208 let s = s.trim().trim_end_matches('Z');
209 let (date, time) = s.split_once('T')?;
210 let mut d = date.split('-');
211 let y: i32 = d.next()?.parse().ok()?;
212 let mo: u32 = d.next()?.parse().ok()?;
213 let day: u32 = d.next()?.parse().ok()?;
214 let mut t = time.split(':');
215 let h: u32 = t.next()?.parse().ok()?;
216 let mi: u32 = t.next()?.parse().ok()?;
217 let se: u32 = t.next()?.parse::<f64>().ok()? as u32;
218 if !(1..=12).contains(&mo) || !(1..=31).contains(&day) {
219 return None;
220 }
221 let mut days: i64 = 0;
222 for yy in 1970..y {
223 days += if is_leap(yy) { 366 } else { 365 };
224 }
225 let md = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
226 for m in 1..mo {
227 days += md[m as usize] as i64;
228 if m == 2 && is_leap(y) {
229 days += 1;
230 }
231 }
232 days += (day as i64) - 1;
233 Some((days as u64) * 86400 + (h as u64) * 3600 + (mi as u64) * 60 + se as u64)
234}
235
236fn is_leap(y: i32) -> bool {
237 y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
238}
239
240fn ok(name: &str, d: &str) -> Check {
241 Check {
242 ok: true,
243 warn: false,
244 name: name.into(),
245 detail: d.into(),
246 }
247}
248fn warn(name: &str, d: &str) -> Check {
249 Check {
250 ok: true,
251 warn: true,
252 name: name.into(),
253 detail: d.into(),
254 }
255}
256fn fail(name: &str, d: &str) -> Check {
257 Check {
258 ok: false,
259 warn: false,
260 name: name.into(),
261 detail: d.into(),
262 }
263}