irontide-format 1.0.2

Shared human-readable formatting helpers for irontide crates
Documentation
//! qBittorrent-compatible peer-flag glyphs.
//!
//! Returns the 15-glyph qBt v2 flag superset for a given `PeerInfo`,
//! paired with a human-readable tooltip. Both the GUI and the Web UI
//! consume this projection — the Web UI joins the chars into an
//! `<abbr>`-decorated string, the GUI joins them into a single
//! `SharedString` for the Peers tab's flag column.
//!
//! See `webui.rs:875` for the original glyph table commentary.

use irontide_session::{PeerInfo, PeerSource};

/// Compute the qBittorrent-compatible peer flags for a single peer.
///
/// Each tuple is `(glyph, tooltip)`. Order is significant — qBt's `WebUI`
/// emits them in the order produced here.
#[must_use]
pub fn peer_flags(p: &PeerInfo) -> Vec<(char, &'static str)> {
    let mut flags = Vec::with_capacity(8);
    if !p.peer_choking && p.num_pieces > 0 && p.am_interested {
        flags.push(('D', "Downloading from peer"));
    }
    if p.am_interested && p.peer_choking {
        flags.push(('d', "We want data but peer is choking us"));
    }
    if !p.am_choking && p.peer_interested {
        flags.push(('U', "Uploading to peer"));
    }
    if p.peer_interested && p.am_choking {
        flags.push(('u', "Peer wants data, we are choking them"));
    }
    if p.am_choking {
        flags.push(('K', "We are choking the peer"));
    }
    if p.am_interested {
        flags.push(('?', "We are interested in the peer"));
    }
    if p.snubbed {
        flags.push(('S', "Peer is snubbed"));
    }
    if p.is_optimistic {
        flags.push(('O', "Optimistic unchoke slot"));
    }
    if p.source == PeerSource::Incoming {
        flags.push(('I', "Incoming connection"));
    }
    if p.source == PeerSource::Dht {
        flags.push(('H', "Discovered via DHT"));
    }
    if p.source == PeerSource::Pex {
        flags.push(('X', "Discovered via PeX (BEP 11)"));
    }
    if p.source == PeerSource::Lsd {
        flags.push(('L', "Discovered via LSD (BEP 14)"));
    }
    if p.is_encrypted {
        flags.push(('E', "Encrypted connection (MSE/PE)"));
    }
    if p.uses_utp {
        flags.push(('P', "Using uTP (BEP 29)"));
    }
    if p.supports_fast {
        flags.push(('F', "Supports fast extension (BEP 6)"));
    }
    flags
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{IpAddr, Ipv4Addr, SocketAddr};

    fn baseline_peer() -> PeerInfo {
        PeerInfo {
            addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 6881),
            client: String::new(),
            peer_choking: false,
            peer_interested: false,
            am_choking: false,
            am_interested: false,
            download_rate: 0,
            upload_rate: 0,
            num_pieces: 0,
            source: PeerSource::Tracker,
            supports_fast: false,
            upload_only: false,
            snubbed: false,
            connected_duration_secs: 0,
            num_pending_requests: 0,
            num_incoming_requests: 0,
            is_optimistic: false,
            is_encrypted: false,
            uses_utp: false,
            uses_holepunch: false,
            in_flight_requests: 0,
            target_pipeline_depth: 0,
        }
    }

    #[test]
    fn glyph_d_downloading() {
        let mut p = baseline_peer();
        p.am_interested = true;
        p.peer_choking = false;
        p.num_pieces = 1;
        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
        assert!(glyphs.contains(&'D'));
        assert!(glyphs.contains(&'?'));
    }

    #[test]
    fn glyph_lowercase_d_choked_but_interested() {
        let mut p = baseline_peer();
        p.am_interested = true;
        p.peer_choking = true;
        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
        assert!(glyphs.contains(&'d'));
        assert!(!glyphs.contains(&'D'));
    }

    #[test]
    fn glyph_u_uploading_and_lowercase_u_choking_them() {
        // Uploading: peer interested + we are not choking
        let mut p = baseline_peer();
        p.peer_interested = true;
        p.am_choking = false;
        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
        assert!(glyphs.contains(&'U'));
        assert!(!glyphs.contains(&'u'));

        // Choking-them: peer interested + we are choking
        let mut p = baseline_peer();
        p.peer_interested = true;
        p.am_choking = true;
        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
        assert!(glyphs.contains(&'u'));
        assert!(glyphs.contains(&'K'));
    }

    #[test]
    fn glyph_source_letters() {
        let cases = [
            (PeerSource::Incoming, 'I'),
            (PeerSource::Dht, 'H'),
            (PeerSource::Pex, 'X'),
            (PeerSource::Lsd, 'L'),
        ];
        for (src, expected) in cases {
            let mut p = baseline_peer();
            p.source = src;
            let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
            assert!(
                glyphs.contains(&expected),
                "source {src:?} should produce glyph {expected}"
            );
        }
    }

    #[test]
    fn glyph_capability_flags_independent() {
        let mut p = baseline_peer();
        p.is_optimistic = true;
        p.is_encrypted = true;
        p.uses_utp = true;
        p.supports_fast = true;
        p.snubbed = true;
        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
        for g in ['O', 'E', 'P', 'F', 'S'] {
            assert!(glyphs.contains(&g), "missing glyph {g}");
        }
    }

    #[test]
    fn empty_baseline_peer_has_no_active_glyphs() {
        let p = baseline_peer();
        let glyphs = peer_flags(&p);
        // Source defaults to Tracker, which has no glyph; baseline has no
        // interest/choke states; capability flags off — so empty.
        assert!(glyphs.is_empty(), "got glyphs: {glyphs:?}");
    }
}