use anyhow::Result;
use tokio::io::AsyncBufReadExt;
use tracing::warn;
pub(crate) const MAX_IRC_LINE: usize = 512;
pub const MAX_IRC_READ_LINE: usize = 8704;
pub(crate) enum IrcLine {
Line(String),
Eof,
TooLong,
}
pub(crate) async fn read_irc_line<R>(
reader: &mut tokio::io::BufReader<R>,
max_len: usize,
) -> std::io::Result<(IrcLine, usize)>
where
R: tokio::io::AsyncRead + Unpin,
{
let mut buf: Vec<u8> = Vec::new();
loop {
let (newline_at, available_len) = {
let available = reader.fill_buf().await?;
(available.iter().position(|&b| b == b'\n'), available.len())
};
if available_len == 0 {
return Ok((IrcLine::Eof, buf.len()));
}
let take = match newline_at {
Some(idx) => idx + 1,
None => available_len,
};
if buf.len() + take > max_len {
reader.consume(take);
return Ok((IrcLine::TooLong, buf.len() + take));
}
{
let available = reader.fill_buf().await?;
buf.extend_from_slice(&available[..take]);
}
reader.consume(take);
if newline_at.is_some() {
let consumed = buf.len();
return Ok((
IrcLine::Line(String::from_utf8_lossy(&buf).into_owned()),
consumed,
));
}
}
}
pub(crate) fn reject_line_breaks(field: &str, value: &str) -> Result<()> {
if let Some(pos) = value.find(['\r', '\n', '\0']) {
return Err(anyhow::anyhow!(
"IRC '{field}' must not contain CR, LF or NUL (found one at byte {pos}): an IRC \
message is terminated by CRLF, so an embedded line break forges a second message \
rather than continuing this one. Send one action per message."
));
}
Ok(())
}
pub(crate) fn reject_not_a_word(field: &str, value: &str) -> Result<()> {
reject_line_breaks(field, value)?;
if value.is_empty() {
return Err(anyhow::anyhow!(
"IRC '{field}' must not be empty: it is a positional parameter, and an empty one \
shifts every parameter after it."
));
}
if value.contains(' ') {
return Err(anyhow::anyhow!(
"IRC '{field}' must not contain a space: IRC parameters are space-separated, so a \
space here shifts every later parameter and the peer parses a different message."
));
}
if value.starts_with(':') {
return Err(anyhow::anyhow!(
"IRC '{field}' must not start with ':': a leading colon marks the trailing \
parameter, which runs to the end of the line."
));
}
Ok(())
}
pub(crate) fn cap_line(what: &str, line: String) -> Vec<u8> {
if line.len() <= MAX_IRC_LINE {
return line.into_bytes();
}
let body = line.strip_suffix("\r\n").unwrap_or(&line);
let mut end = MAX_IRC_LINE - 2;
while end > 0 && !body.is_char_boundary(end) {
end -= 1;
}
warn!(
"IRC {} produced a {}-byte line; truncated to the RFC 1459 limit of {} bytes \
(including CRLF)",
what,
line.len(),
MAX_IRC_LINE
);
let mut out = String::with_capacity(MAX_IRC_LINE);
out.push_str(&body[..end]);
out.push_str("\r\n");
out.into_bytes()
}