Skip to main content

deaddrop_app/
lib.rs

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}