use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct Isupport {
tokens: HashMap<String, String>,
}
#[allow(dead_code)]
impl Isupport {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn parse_tokens(&mut self, tokens: &[&str]) {
for &token in tokens {
if let Some(negated) = token.strip_prefix('-') {
self.tokens.remove(negated);
} else if let Some((key, value)) = token.split_once('=') {
self.tokens.insert(key.to_string(), value.to_string());
} else {
self.tokens.insert(token.to_string(), String::new());
}
}
}
#[must_use]
pub fn prefix_map(&self) -> Vec<(char, char)> {
let Some(value) = self.tokens.get("PREFIX") else {
return vec![('o', '@'), ('v', '+')];
};
let Some(rest) = value.strip_prefix('(') else {
return vec![('o', '@'), ('v', '+')];
};
let Some((modes, prefixes)) = rest.split_once(')') else {
return vec![('o', '@'), ('v', '+')];
};
modes.chars().zip(prefixes.chars()).collect()
}
#[must_use]
pub fn chanmode_types(&self) -> (String, String, String, String) {
let Some(value) = self.tokens.get("CHANMODES") else {
return (String::new(), String::new(), String::new(), String::new());
};
let mut parts = value.splitn(4, ',');
let a = parts.next().unwrap_or("").to_string();
let b = parts.next().unwrap_or("").to_string();
let c = parts.next().unwrap_or("").to_string();
let d = parts.next().unwrap_or("").to_string();
(a, b, c, d)
}
#[must_use]
pub fn network(&self) -> Option<&str> {
self.tokens.get("NETWORK").map(String::as_str)
}
#[must_use]
pub fn has_whox(&self) -> bool {
self.tokens.contains_key("WHOX")
}
#[must_use]
pub fn max_modes(&self) -> usize {
self.tokens
.get("MODES")
.and_then(|v| v.parse().ok())
.unwrap_or(3)
}
#[must_use]
pub fn statusmsg(&self) -> &str {
self.tokens.get("STATUSMSG").map_or("", String::as_str)
}
#[must_use]
pub fn casemapping(&self) -> &str {
self.tokens
.get("CASEMAPPING")
.map_or("rfc1459", String::as_str)
}
#[must_use]
pub fn channel_len(&self) -> usize {
self.tokens
.get("CHANNELLEN")
.and_then(|v| v.parse().ok())
.unwrap_or(200)
}
#[must_use]
pub fn nick_len(&self) -> usize {
self.tokens
.get("NICKLEN")
.and_then(|v| v.parse().ok())
.unwrap_or(9)
}
#[must_use]
pub fn topic_len(&self) -> usize {
self.tokens
.get("TOPICLEN")
.and_then(|v| v.parse().ok())
.unwrap_or(307)
}
#[must_use]
pub fn chan_types(&self) -> &str {
self.tokens.get("CHANTYPES").map_or("#&", String::as_str)
}
#[must_use]
pub fn extban(&self) -> Option<(char, String)> {
let value = self.tokens.get("EXTBAN")?;
let (prefix_str, types) = value.split_once(',')?;
let prefix = prefix_str.chars().next()?;
Some((prefix, types.to_string()))
}
#[must_use]
pub fn supports_multi_target_mode(&self) -> bool {
let Some(targmax) = self.tokens.get("TARGMAX") else {
return true; };
targmax.split(',').any(|entry| {
entry
.split_once(':')
.is_some_and(|(cmd, limit)| cmd == "MODE" && limit != "1" && limit != "0")
})
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.tokens.get(key).map(String::as_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_key_value_tokens() {
let mut is = Isupport::new();
is.parse_tokens(&["NETWORK=Libera.Chat", "MODES=4", "CHANTYPES=#"]);
assert_eq!(is.network(), Some("Libera.Chat"));
assert_eq!(is.max_modes(), 4);
assert_eq!(is.chan_types(), "#");
}
#[test]
fn parse_bare_token() {
let mut is = Isupport::new();
is.parse_tokens(&["WHOX", "SAFELIST"]);
assert!(is.has_whox());
assert_eq!(is.get("SAFELIST"), Some(""));
}
#[test]
fn negated_token() {
let mut is = Isupport::new();
is.parse_tokens(&["WHOX"]);
assert!(is.has_whox());
is.parse_tokens(&["-WHOX"]);
assert!(!is.has_whox());
}
#[test]
fn prefix_parsing_standard() {
let mut is = Isupport::new();
is.parse_tokens(&["PREFIX=(ov)@+"]);
let map = is.prefix_map();
assert_eq!(map, vec![('o', '@'), ('v', '+')]);
}
#[test]
fn prefix_parsing_extended() {
let mut is = Isupport::new();
is.parse_tokens(&["PREFIX=(qaohv)~&@%+"]);
let map = is.prefix_map();
assert_eq!(
map,
vec![('q', '~'), ('a', '&'), ('o', '@'), ('h', '%'), ('v', '+'),]
);
}
#[test]
fn prefix_default_when_absent() {
let is = Isupport::new();
let map = is.prefix_map();
assert_eq!(map, vec![('o', '@'), ('v', '+')]);
}
#[test]
fn chanmodes_parsing() {
let mut is = Isupport::new();
is.parse_tokens(&["CHANMODES=beI,k,l,imnpstaqrRcOAQKVCuzNSMTGZ"]);
let (a, b, c, d) = is.chanmode_types();
assert_eq!(a, "beI");
assert_eq!(b, "k");
assert_eq!(c, "l");
assert_eq!(d, "imnpstaqrRcOAQKVCuzNSMTGZ");
}
#[test]
fn chanmodes_default_when_absent() {
let is = Isupport::new();
let (a, b, c, d) = is.chanmode_types();
assert!(a.is_empty());
assert!(b.is_empty());
assert!(c.is_empty());
assert!(d.is_empty());
}
#[test]
fn casemapping_values() {
let mut is = Isupport::new();
assert_eq!(is.casemapping(), "rfc1459");
is.parse_tokens(&["CASEMAPPING=ascii"]);
assert_eq!(is.casemapping(), "ascii");
}
#[test]
fn defaults_for_missing_tokens() {
let is = Isupport::new();
assert_eq!(is.network(), None);
assert!(!is.has_whox());
assert_eq!(is.max_modes(), 3);
assert_eq!(is.statusmsg(), "");
assert_eq!(is.casemapping(), "rfc1459");
assert_eq!(is.channel_len(), 200);
assert_eq!(is.nick_len(), 9);
assert_eq!(is.topic_len(), 307);
assert_eq!(is.chan_types(), "#&");
assert_eq!(is.extban(), None);
}
#[test]
fn extban_parsing() {
let mut is = Isupport::new();
is.parse_tokens(&["EXTBAN=~,qaojrsnSR"]);
let (prefix, types) = is.extban().expect("EXTBAN should parse");
assert_eq!(prefix, '~');
assert_eq!(types, "qaojrsnSR");
}
#[test]
fn extban_missing() {
let is = Isupport::new();
assert_eq!(is.extban(), None);
}
#[test]
fn statusmsg_parsing() {
let mut is = Isupport::new();
is.parse_tokens(&["STATUSMSG=@+"]);
assert_eq!(is.statusmsg(), "@+");
}
#[test]
fn length_tokens() {
let mut is = Isupport::new();
is.parse_tokens(&["NICKLEN=30", "CHANNELLEN=50", "TOPICLEN=390"]);
assert_eq!(is.nick_len(), 30);
assert_eq!(is.channel_len(), 50);
assert_eq!(is.topic_len(), 390);
}
#[test]
fn multiple_parse_calls_merge() {
let mut is = Isupport::new();
is.parse_tokens(&["NETWORK=Freenode", "MODES=3"]);
is.parse_tokens(&["NETWORK=Libera.Chat", "WHOX"]);
assert_eq!(is.network(), Some("Libera.Chat"));
assert_eq!(is.max_modes(), 3);
assert!(is.has_whox());
}
#[test]
fn full_real_world_isupport() {
let mut is = Isupport::new();
is.parse_tokens(&[
"CALLERID",
"CASEMAPPING=rfc1459",
"DEAF=D",
"KICKLEN=255",
"MODES=4",
]);
is.parse_tokens(&[
"PREFIX=(ov)@+",
"STATUSMSG=@+",
"EXCEPTS=e",
"INVEX=I",
"CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz",
]);
is.parse_tokens(&[
"CHANTYPES=#",
"NETWORK=Libera.Chat",
"NICKLEN=16",
"CHANNELLEN=50",
"TOPICLEN=390",
"WHOX",
]);
assert_eq!(is.casemapping(), "rfc1459");
assert_eq!(is.max_modes(), 4);
assert_eq!(is.prefix_map(), vec![('o', '@'), ('v', '+')]);
assert_eq!(is.statusmsg(), "@+");
assert_eq!(is.chan_types(), "#");
assert_eq!(is.network(), Some("Libera.Chat"));
assert_eq!(is.nick_len(), 16);
assert_eq!(is.channel_len(), 50);
assert_eq!(is.topic_len(), 390);
assert!(is.has_whox());
let (a, b, c, d) = is.chanmode_types();
assert_eq!(a, "eIbq");
assert_eq!(b, "k");
assert_eq!(c, "flj");
assert_eq!(d, "CFLMPQScgimnprstuz");
}
#[test]
fn multi_target_mode_with_targmax_no_mode() {
let mut is = Isupport::new();
is.parse_tokens(&[
"TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR:",
]);
assert!(!is.supports_multi_target_mode());
}
#[test]
fn multi_target_mode_with_targmax_mode_listed() {
let mut is = Isupport::new();
is.parse_tokens(&["TARGMAX=NAMES:1,MODE:4,PRIVMSG:4"]);
assert!(is.supports_multi_target_mode());
}
#[test]
fn multi_target_mode_without_targmax() {
let is = Isupport::new();
assert!(is.supports_multi_target_mode());
}
#[test]
fn multi_target_mode_targmax_mode_limited_to_one() {
let mut is = Isupport::new();
is.parse_tokens(&["TARGMAX=MODE:1,PRIVMSG:4"]);
assert!(!is.supports_multi_target_mode());
}
#[test]
fn multi_target_mode_targmax_mode_unlimited() {
let mut is = Isupport::new();
is.parse_tokens(&["TARGMAX=MODE:,PRIVMSG:4"]);
assert!(is.supports_multi_target_mode());
}
}