deaddrop-app 2.6.0

DeadDrop CLI, daemon, relay, simulator, and examples
Documentation
use deaddrop_core::config::Config;
use deaddrop_core::protocol::Message;
use deaddrop_core::store::Store;
use deaddrop_core::{PROTOCOL_LABEL, Result};
use std::path::Path;

pub mod daemon_service;

#[derive(Debug, Clone)]
pub struct Check {
    pub ok: bool,
    pub warn: bool,
    pub name: String,
    pub detail: String,
}

pub fn doctor(data_dir: &Path, cfg: &Config) -> Result<Vec<Check>> {
    let mut out = Vec::new();
    let id_path = data_dir.join("identities").join("identity.json");
    if id_path.exists() {
        match Store::open(data_dir, Default::default()).and_then(|s| s.load_identity()) {
            Ok(id) => out.push(ok("identity", &id.peer_id.to_string())),
            Err(e) => out.push(fail("identity", &e.to_string())),
        }
    } else {
        out.push(fail("identity", "run `dd init`"));
    }
    out.push(ok("keystore", "identity.json (OS file permissions apply)"));
    let probe = data_dir.join(".write-probe");
    match std::fs::write(&probe, b"ok") {
        Ok(()) => {
            let _ = std::fs::remove_file(&probe);
            out.push(ok("permissions", &data_dir.display().to_string()));
        }
        Err(e) => out.push(fail("permissions", &e.to_string())),
    }
    match Store::open(data_dir, Default::default()) {
        Ok(s) => match s.verify() {
            Ok(p) if p.is_empty() => out.push(ok("database", "sqlite + chunks")),
            Ok(p) => out.push(warn(
                "database",
                &format!("{} issues (Drop bodies kept)", p.len()),
            )),
            Err(e) => out.push(fail("database", &e.to_string())),
        },
        Err(e) => out.push(fail("database", &e.to_string())),
    }
    out.push(ok(
        "crypto backend",
        "DefaultProvider (Ed25519/X25519/ChaCha20-Poly1305/BLAKE3)",
    ));
    if let Ok(s) = Store::open(data_dir, Default::default()) {
        match s.stats() {
            Ok(st) => {
                let cap = deaddrop_core::config::Config::parse_bytes(&cfg.storage.maximum);
                if st.physical_size + 64 * 1024 * 1024 > cap && cap > 0 {
                    out.push(warn(
                        "disk capacity",
                        &format!(
                            "{} used of {}",
                            deaddrop_core::store::format_bytes(st.physical_size),
                            cfg.storage.maximum
                        ),
                    ));
                } else {
                    out.push(ok(
                        "disk capacity",
                        &deaddrop_core::store::format_bytes(st.physical_size),
                    ));
                }
                let now = deaddrop_core::store::unix_now();
                let stale = s
                    .all_encounters()
                    .unwrap_or_default()
                    .into_iter()
                    .filter(|(_, e)| now.saturating_sub(e.last_seen) > 30 * 86_400)
                    .count();
                if stale > 0 {
                    out.push(warn("stale peers", &format!("{stale} not seen in 30d")));
                } else {
                    out.push(ok("stale peers", "none"));
                }
                match s.verify() {
                    Ok(p) if p.is_empty() => out.push(ok("Drop store", "chunk hashes match")),
                    Ok(p) => out.push(warn("corrupted Drops", &p.join("; "))),
                    Err(e) => out.push(fail("Drop store", &e.to_string())),
                }
            }
            Err(e) => out.push(fail("storage stats", &e.to_string())),
        }
    }
    out.push(if cfg.discovery.lan && cfg.discovery.mode.advertise_lan() {
        ok(
            "LAN discovery",
            &format!("UDP {} ({:?})", cfg.discovery.lan_port, cfg.discovery.mode),
        )
    } else {
        warn("LAN discovery", "disabled or hidden")
    });
    out.push(if cfg.transport.quic {
        warn(
            "QUIC",
            "enabled in config; default `dd serve` listens TCP. QUIC types are EXPERIMENTAL",
        )
    } else {
        warn("QUIC", "disabled in config")
    });
    out.push(warn(
        "Bluetooth",
        "unavailable (no BLE transport in this tree)",
    ));
    out.push(ok("routing engine", &cfg.routing.strategy));
    let now = deaddrop_core::store::unix_now();
    if now < 1_700_000_000 {
        out.push(warn(
            "time synchronization",
            "system clock looks before 2023; sealed-until policy may mis-fire",
        ));
    } else {
        out.push(ok(
            "time synchronization",
            "local clock present (no NTP client)",
        ));
    }
    match deaddrop_net::control::read_info(data_dir) {
        Ok(Some(info)) => match std::net::TcpStream::connect_timeout(
            &info
                .listen
                .parse()
                .unwrap_or_else(|_| "127.0.0.1:1".parse().expect("literal")),
            std::time::Duration::from_millis(200),
        ) {
            Ok(_) => out.push(ok(
                "daemon",
                &format!("pid {} control {}", info.pid, info.listen),
            )),
            Err(_) => out.push(warn(
                "daemon",
                "control.json present but not reachable — run `dd daemon start`",
            )),
        },
        _ => out.push(warn("daemon", "not running (CLI store still works)")),
    }
    out.push(ok("protocol compatibility", PROTOCOL_LABEL));
    out.push(ok(
        "network interfaces",
        "OS default; LAN uses UDP broadcast",
    ));
    Ok(out)
}

pub fn doctor_repair(data_dir: &Path) -> Result<Vec<String>> {
    let store = Store::open(data_dir, Default::default())?;
    store.repair_safe()
}

pub fn inspect_protocol_bytes(bytes: &[u8]) -> String {
    let payload = if bytes.len() >= 4 {
        let n = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
        if n + 4 == bytes.len() {
            &bytes[4..]
        } else {
            bytes
        }
    } else {
        bytes
    };
    match deaddrop_core::protocol::decode_cbor::<Message>(payload) {
        Ok(m) => format!(
            "Frame Type      {}\nVersion         {PROTOCOL_LABEL}\nPayload Size    {} bytes\nAuthenticated   (session layer; inspect is offline)\n",
            m.name(),
            payload.len()
        ),
        Err(e) => {
            format!("decode error: {e}\n(not a DDP Message; JSON/debug export is separate)\n")
        }
    }
}

pub fn format_uptime(secs: u64) -> String {
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    format!("{h:02}:{m:02}:{s:02}")
}

pub fn parse_duration_or_rfc3339(s: &str) -> Option<u64> {
    let t = s.trim();
    if let Some(d) = t.strip_suffix('d') {
        return d.parse::<u64>().ok().map(|n| n * 86400);
    }
    if let Some(h) = t.strip_suffix('h') {
        return h.parse::<u64>().ok().map(|n| n * 3600);
    }
    if let Some(m) = t.strip_suffix('m') {
        return m.parse::<u64>().ok().map(|n| n * 60);
    }
    if let Some(sec) = t.strip_suffix('s') {
        return sec.parse::<u64>().ok();
    }
    if t.contains('T') {
        return parse_rfc3339_utc(t);
    }
    t.parse::<u64>().ok()
}

fn parse_rfc3339_utc(s: &str) -> Option<u64> {
    let s = s.trim().trim_end_matches('Z');
    let (date, time) = s.split_once('T')?;
    let mut d = date.split('-');
    let y: i32 = d.next()?.parse().ok()?;
    let mo: u32 = d.next()?.parse().ok()?;
    let day: u32 = d.next()?.parse().ok()?;
    let mut t = time.split(':');
    let h: u32 = t.next()?.parse().ok()?;
    let mi: u32 = t.next()?.parse().ok()?;
    let se: u32 = t.next()?.parse::<f64>().ok()? as u32;
    if !(1..=12).contains(&mo) || !(1..=31).contains(&day) {
        return None;
    }
    let mut days: i64 = 0;
    for yy in 1970..y {
        days += if is_leap(yy) { 366 } else { 365 };
    }
    let md = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    for m in 1..mo {
        days += md[m as usize] as i64;
        if m == 2 && is_leap(y) {
            days += 1;
        }
    }
    days += (day as i64) - 1;
    Some((days as u64) * 86400 + (h as u64) * 3600 + (mi as u64) * 60 + se as u64)
}

fn is_leap(y: i32) -> bool {
    y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
}

fn ok(name: &str, d: &str) -> Check {
    Check {
        ok: true,
        warn: false,
        name: name.into(),
        detail: d.into(),
    }
}
fn warn(name: &str, d: &str) -> Check {
    Check {
        ok: true,
        warn: true,
        name: name.into(),
        detail: d.into(),
    }
}
fn fail(name: &str, d: &str) -> Check {
    Check {
        ok: false,
        warn: false,
        name: name.into(),
        detail: d.into(),
    }
}