Skip to main content

irontide_format/
peer_flags.rs

1//! qBittorrent-compatible peer-flag glyphs.
2//!
3//! Returns the 15-glyph qBt v2 flag superset for a given `PeerInfo`,
4//! paired with a human-readable tooltip. Both the GUI and the Web UI
5//! consume this projection — the Web UI joins the chars into an
6//! `<abbr>`-decorated string, the GUI joins them into a single
7//! `SharedString` for the Peers tab's flag column.
8//!
9//! See `webui.rs:875` for the original glyph table commentary.
10
11use irontide_session::{PeerInfo, PeerSource};
12
13/// Compute the qBittorrent-compatible peer flags for a single peer.
14///
15/// Each tuple is `(glyph, tooltip)`. Order is significant — qBt's `WebUI`
16/// emits them in the order produced here.
17#[must_use]
18pub fn peer_flags(p: &PeerInfo) -> Vec<(char, &'static str)> {
19    let mut flags = Vec::with_capacity(8);
20    if !p.peer_choking && p.num_pieces > 0 && p.am_interested {
21        flags.push(('D', "Downloading from peer"));
22    }
23    if p.am_interested && p.peer_choking {
24        flags.push(('d', "We want data but peer is choking us"));
25    }
26    if !p.am_choking && p.peer_interested {
27        flags.push(('U', "Uploading to peer"));
28    }
29    if p.peer_interested && p.am_choking {
30        flags.push(('u', "Peer wants data, we are choking them"));
31    }
32    if p.am_choking {
33        flags.push(('K', "We are choking the peer"));
34    }
35    if p.am_interested {
36        flags.push(('?', "We are interested in the peer"));
37    }
38    if p.snubbed {
39        flags.push(('S', "Peer is snubbed"));
40    }
41    if p.is_optimistic {
42        flags.push(('O', "Optimistic unchoke slot"));
43    }
44    if p.source == PeerSource::Incoming {
45        flags.push(('I', "Incoming connection"));
46    }
47    if p.source == PeerSource::Dht {
48        flags.push(('H', "Discovered via DHT"));
49    }
50    if p.source == PeerSource::Pex {
51        flags.push(('X', "Discovered via PeX (BEP 11)"));
52    }
53    if p.source == PeerSource::Lsd {
54        flags.push(('L', "Discovered via LSD (BEP 14)"));
55    }
56    if p.is_encrypted {
57        flags.push(('E', "Encrypted connection (MSE/PE)"));
58    }
59    if p.uses_utp {
60        flags.push(('P', "Using uTP (BEP 29)"));
61    }
62    if p.supports_fast {
63        flags.push(('F', "Supports fast extension (BEP 6)"));
64    }
65    flags
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
72
73    fn baseline_peer() -> PeerInfo {
74        PeerInfo {
75            addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 6881),
76            client: String::new(),
77            peer_choking: false,
78            peer_interested: false,
79            am_choking: false,
80            am_interested: false,
81            download_rate: 0,
82            upload_rate: 0,
83            num_pieces: 0,
84            source: PeerSource::Tracker,
85            supports_fast: false,
86            upload_only: false,
87            snubbed: false,
88            connected_duration_secs: 0,
89            num_pending_requests: 0,
90            num_incoming_requests: 0,
91            is_optimistic: false,
92            is_encrypted: false,
93            uses_utp: false,
94            uses_holepunch: false,
95            in_flight_requests: 0,
96            target_pipeline_depth: 0,
97        }
98    }
99
100    #[test]
101    fn glyph_d_downloading() {
102        let mut p = baseline_peer();
103        p.am_interested = true;
104        p.peer_choking = false;
105        p.num_pieces = 1;
106        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
107        assert!(glyphs.contains(&'D'));
108        assert!(glyphs.contains(&'?'));
109    }
110
111    #[test]
112    fn glyph_lowercase_d_choked_but_interested() {
113        let mut p = baseline_peer();
114        p.am_interested = true;
115        p.peer_choking = true;
116        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
117        assert!(glyphs.contains(&'d'));
118        assert!(!glyphs.contains(&'D'));
119    }
120
121    #[test]
122    fn glyph_u_uploading_and_lowercase_u_choking_them() {
123        // Uploading: peer interested + we are not choking
124        let mut p = baseline_peer();
125        p.peer_interested = true;
126        p.am_choking = false;
127        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
128        assert!(glyphs.contains(&'U'));
129        assert!(!glyphs.contains(&'u'));
130
131        // Choking-them: peer interested + we are choking
132        let mut p = baseline_peer();
133        p.peer_interested = true;
134        p.am_choking = true;
135        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
136        assert!(glyphs.contains(&'u'));
137        assert!(glyphs.contains(&'K'));
138    }
139
140    #[test]
141    fn glyph_source_letters() {
142        let cases = [
143            (PeerSource::Incoming, 'I'),
144            (PeerSource::Dht, 'H'),
145            (PeerSource::Pex, 'X'),
146            (PeerSource::Lsd, 'L'),
147        ];
148        for (src, expected) in cases {
149            let mut p = baseline_peer();
150            p.source = src;
151            let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
152            assert!(
153                glyphs.contains(&expected),
154                "source {src:?} should produce glyph {expected}"
155            );
156        }
157    }
158
159    #[test]
160    fn glyph_capability_flags_independent() {
161        let mut p = baseline_peer();
162        p.is_optimistic = true;
163        p.is_encrypted = true;
164        p.uses_utp = true;
165        p.supports_fast = true;
166        p.snubbed = true;
167        let glyphs: Vec<char> = peer_flags(&p).into_iter().map(|(c, _)| c).collect();
168        for g in ['O', 'E', 'P', 'F', 'S'] {
169            assert!(glyphs.contains(&g), "missing glyph {g}");
170        }
171    }
172
173    #[test]
174    fn empty_baseline_peer_has_no_active_glyphs() {
175        let p = baseline_peer();
176        let glyphs = peer_flags(&p);
177        // Source defaults to Tracker, which has no glyph; baseline has no
178        // interest/choke states; capability flags off — so empty.
179        assert!(glyphs.is_empty(), "got glyphs: {glyphs:?}");
180    }
181}