irc: split PRIVMSG on embedded newlines + widen host overhead
Two fixes to send_privmsg, both surfaced by correspondents reporting truncated messages: 1. Multi-line content (code blocks, formatted text) sent as a single PRIVMSG was being truncated at the first '\n' by the IRC server — newlines are end-of-command markers. Split the message on newlines and send each line as its own PRIVMSG; skip empty lines since most servers reject empty PRIVMSGs. 2. Overhead computation assumed a host field of 63 bytes. OFTC's cloaked hostmasks can be longer, occasionally pushing the server- prepended prefix past 512 bytes and causing silent truncation. Raise the host budget to 80 and align the formula with the actual ':nick!~nick@host' prefix shape. Also extended the word-boundary lookback from a fixed 10 chars to max_msg / 4 — dense content (code) rarely had a space within 10 chars of the length cap, so we were falling back to the char boundary and splitting mid-word. Checking bytes[j-1] for a space (instead of bytes[j]) drops leading whitespace from the rest-fragment. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
85799587cc
commit
5908b837e8
1 changed files with 39 additions and 21 deletions
|
|
@ -237,11 +237,19 @@ impl State {
|
|||
async fn send_privmsg(&mut self, target: &str, msg: &str) -> io::Result<()> {
|
||||
// Send PRIVMSG, which is used for both private and channel messages.
|
||||
// Splits into multiple fragments if necessary.
|
||||
// IRC max line = 512 bytes including CRLF. The server prepends
|
||||
// our prefix when relaying: ":nick!~user@host PRIVMSG target :msg\r\n"
|
||||
//
|
||||
// Two constraints:
|
||||
// 1. IRC max line = 512 bytes including CRLF. The server prepends
|
||||
// our prefix when relaying: ":nick!~user@host PRIVMSG target :msg\r\n"
|
||||
// So per-PRIVMSG message content must fit in 512 - overhead.
|
||||
// 2. Embedded '\n' in the message would be interpreted by the
|
||||
// server as an end-of-command marker, truncating us. Split
|
||||
// on newlines first and send each line as its own PRIVMSG.
|
||||
//
|
||||
// User is often ~nick (nick_len + 1). Host is up to 63 bytes.
|
||||
// Cloaked OFTC hosts can be longer - pad the budget.
|
||||
let nick_len = self.config.nick.len();
|
||||
let overhead = 1 + nick_len + 2 + nick_len + 1 + 63
|
||||
let overhead = 1 + nick_len + 1 + (nick_len + 1) + 1 + 80
|
||||
+ " PRIVMSG ".len() + target.len() + " :".len() + 2;
|
||||
let max_msg = 512_usize.saturating_sub(overhead);
|
||||
|
||||
|
|
@ -249,24 +257,34 @@ impl State {
|
|||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "target too long"));
|
||||
}
|
||||
|
||||
// Split on UTF-8 char boundaries
|
||||
let mut remaining = msg;
|
||||
while !remaining.is_empty() {
|
||||
let split_at = if remaining.len() <= max_msg {
|
||||
remaining.len()
|
||||
} else {
|
||||
// Find last char boundary at or before max_msg
|
||||
let mut i = max_msg;
|
||||
while i > 0 && !remaining.is_char_boundary(i) { i -= 1; }
|
||||
// To avoid splitting mid-word, see if there was a space recently
|
||||
let mut j = i;
|
||||
while j > 1 && j > i-10 && remaining.as_bytes()[j] != b' ' { j -= 1; }
|
||||
if remaining.as_bytes()[j] == b' ' { j }
|
||||
else if i == 0 { max_msg } else { i }
|
||||
};
|
||||
let (chunk, rest) = remaining.split_at(split_at);
|
||||
self.send_raw(&format!("PRIVMSG {target} :{chunk}")).await?;
|
||||
remaining = rest;
|
||||
for line in msg.split('\n') {
|
||||
let mut remaining = line;
|
||||
// Empty lines (blank paragraph breaks) can't be sent as empty
|
||||
// PRIVMSGs - most IRC servers reject them. Skip.
|
||||
if remaining.is_empty() { continue; }
|
||||
loop {
|
||||
let split_at = if remaining.len() <= max_msg {
|
||||
remaining.len()
|
||||
} else {
|
||||
// Find last char boundary at or before max_msg.
|
||||
let mut i = max_msg;
|
||||
while i > 0 && !remaining.is_char_boundary(i) { i -= 1; }
|
||||
// Prefer splitting at a word boundary - look back up to
|
||||
// max_msg/4 chars for a space. With dense content (code)
|
||||
// we may not find one; fall back to the char boundary.
|
||||
let lookback = max_msg / 4;
|
||||
let bytes = remaining.as_bytes();
|
||||
let mut j = i;
|
||||
while j > 0 && (i - j) < lookback && bytes[j - 1] != b' ' {
|
||||
j -= 1;
|
||||
}
|
||||
if j > 0 && bytes[j - 1] == b' ' { j } else { i }
|
||||
};
|
||||
let (chunk, rest) = remaining.split_at(split_at);
|
||||
self.send_raw(&format!("PRIVMSG {target} :{chunk}")).await?;
|
||||
remaining = rest;
|
||||
if remaining.is_empty() { break; }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue