#![warn(missing_docs, rustdoc::missing_crate_level_docs)]
#![deny(unsafe_code)]
pub mod error;
pub mod message;
pub mod command;
pub mod capabilities;
pub mod sasl;
pub mod validation;
pub mod replies;
pub mod iron;
pub mod admin;
#[cfg(feature = "bleeding-edge")]
pub mod bleeding_edge;
pub use error::{IronError, Result};
pub use message::IrcMessage;
pub use command::Command;
pub use capabilities::{Capability, CapabilitySet, CapabilityHandler};
pub use replies::Reply;
pub use utils::ChannelType;
pub use iron::{IronSession, IronVersion, IronNegotiationResult, IronChannelHandler, ChannelJoinResult, IronChannelError};
pub use admin::{AdminOperation, MemberOperation, BanOperation, KeyOperation, MemberRole, ChannelMode,
ChannelSettings, AdminResult, ChannelAdmin, Permission};
#[cfg(feature = "bleeding-edge")]
pub use bleeding_edge::{MessageReply, MessageReaction, ReactionAction};
pub mod constants {
pub const MAX_MESSAGE_LENGTH: usize = 512;
pub const MAX_TAG_LENGTH: usize = 8191;
pub const MAX_PARAMS: usize = 15;
pub const MAX_CAPABILITY_NAME_LENGTH: usize = 64;
pub const MAX_NICK_LENGTH: usize = 32;
pub const MAX_CHANNEL_LENGTH: usize = 50;
pub const DEFAULT_IRC_PORT: u16 = 6667;
pub const DEFAULT_IRCS_PORT: u16 = 6697;
}
pub mod utils {
use crate::constants::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChannelType {
IrcGlobal,
IrcLocal,
LegionEncrypted,
Invalid,
}
pub fn get_channel_type(channel: &str) -> ChannelType {
if channel.is_empty() {
return ChannelType::Invalid;
}
match channel.chars().next().unwrap() {
'#' => ChannelType::IrcGlobal,
'&' => ChannelType::IrcLocal,
'!' => ChannelType::LegionEncrypted,
_ => ChannelType::Invalid,
}
}
pub fn is_legion_encrypted_channel(channel: &str) -> bool {
matches!(get_channel_type(channel), ChannelType::LegionEncrypted)
}
#[deprecated(note = "Use is_legion_encrypted_channel instead")]
pub fn is_iron_encrypted_channel(channel: &str) -> bool {
is_legion_encrypted_channel(channel)
}
pub fn is_standard_irc_channel(channel: &str) -> bool {
matches!(get_channel_type(channel), ChannelType::IrcGlobal | ChannelType::IrcLocal)
}
pub fn is_valid_nick(nick: &str) -> bool {
if nick.is_empty() || nick.len() > MAX_NICK_LENGTH {
return false;
}
let first = nick.chars().next().unwrap();
if !first.is_ascii_alphabetic() && !matches!(first, '[' | ']' | '\\' | '`' | '_' | '^' | '{' | '|' | '}') {
return false;
}
nick.chars().skip(1).all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '[' | ']' | '\\' | '`' | '_' | '^' | '{' | '|' | '}' | '-')
})
}
pub fn is_valid_channel(channel: &str) -> bool {
if channel.is_empty() || channel.len() > MAX_CHANNEL_LENGTH {
return false;
}
if !channel.starts_with('#') && !channel.starts_with('&') {
return false;
}
!channel.chars().any(|c| c.is_control() || c == ' ' || c == ',' || c == '\x07')
}
pub fn is_valid_legion_channel(channel: &str) -> bool {
if channel.is_empty() || channel.len() > MAX_CHANNEL_LENGTH {
return false;
}
if !channel.starts_with('!') {
return false;
}
!channel.chars().any(|c| c.is_control() || c == ' ' || c == ',' || c == '\x07')
}
#[deprecated(note = "Use is_valid_legion_channel instead")]
pub fn is_valid_iron_channel(channel: &str) -> bool {
is_valid_legion_channel(channel)
}
pub fn is_valid_any_channel(channel: &str) -> bool {
is_valid_channel(channel) || is_valid_legion_channel(channel)
}
pub fn escape_message(text: &str) -> String {
text.replace('\r', "")
.replace('\n', " ")
.replace('\0', "")
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::utils::*;
#[test]
fn test_valid_nicks() {
assert!(is_valid_nick("Alice"));
assert!(is_valid_nick("Bob123"));
assert!(is_valid_nick("user_name"));
assert!(is_valid_nick("[Bot]"));
assert!(is_valid_nick("test-user"));
}
#[test]
fn test_invalid_nicks() {
assert!(!is_valid_nick(""));
assert!(!is_valid_nick("123user")); assert!(!is_valid_nick("user name")); assert!(!is_valid_nick(&"a".repeat(50))); }
#[test]
fn test_valid_channels() {
assert!(is_valid_channel("#general"));
assert!(is_valid_channel("&local"));
assert!(is_valid_channel("#test-channel"));
assert!(is_valid_channel("#channel123"));
}
#[test]
fn test_invalid_channels() {
assert!(!is_valid_channel(""));
assert!(!is_valid_channel("general")); assert!(!is_valid_channel("#test channel")); assert!(!is_valid_channel("#test,channel")); assert!(!is_valid_channel(&format!("#{}", "a".repeat(60)))); }
#[test]
fn test_valid_legion_channels() {
assert!(is_valid_legion_channel("!encrypted"));
assert!(is_valid_legion_channel("!secure-chat"));
assert!(is_valid_legion_channel("!room123"));
assert!(is_valid_legion_channel("!test_channel"));
}
#[test]
fn test_invalid_legion_channels() {
assert!(!is_valid_legion_channel(""));
assert!(!is_valid_legion_channel("encrypted")); assert!(!is_valid_legion_channel("#encrypted")); assert!(!is_valid_legion_channel("!test channel")); assert!(!is_valid_legion_channel("!test,channel")); assert!(!is_valid_legion_channel(&format!("!{}", "a".repeat(60)))); }
#[test]
fn test_backward_compatibility_iron_channels() {
#[allow(deprecated)]
fn test_old_function() {
assert!(is_valid_iron_channel("!encrypted"));
assert!(!is_valid_iron_channel("#encrypted"));
}
test_old_function();
}
#[test]
fn test_channel_type_detection() {
use utils::*;
assert_eq!(get_channel_type("#general"), ChannelType::IrcGlobal);
assert_eq!(get_channel_type("&local"), ChannelType::IrcLocal);
assert_eq!(get_channel_type("!encrypted"), ChannelType::LegionEncrypted);
assert_eq!(get_channel_type("invalid"), ChannelType::Invalid);
assert_eq!(get_channel_type(""), ChannelType::Invalid);
assert!(is_legion_encrypted_channel("!encrypted"));
assert!(!is_legion_encrypted_channel("#general"));
#[allow(deprecated)]
{
assert!(is_iron_encrypted_channel("!encrypted"));
assert!(!is_iron_encrypted_channel("#general"));
}
assert!(is_standard_irc_channel("#general"));
assert!(is_standard_irc_channel("&local"));
assert!(!is_standard_irc_channel("!encrypted"));
}
}