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<()> {
|
async fn send_privmsg(&mut self, target: &str, msg: &str) -> io::Result<()> {
|
||||||
// Send PRIVMSG, which is used for both private and channel messages.
|
// Send PRIVMSG, which is used for both private and channel messages.
|
||||||
// Splits into multiple fragments if necessary.
|
// Splits into multiple fragments if necessary.
|
||||||
// IRC max line = 512 bytes including CRLF. The server prepends
|
//
|
||||||
|
// 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"
|
// 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.
|
// 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 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;
|
+ " PRIVMSG ".len() + target.len() + " :".len() + 2;
|
||||||
let max_msg = 512_usize.saturating_sub(overhead);
|
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"));
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "target too long"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split on UTF-8 char boundaries
|
for line in msg.split('\n') {
|
||||||
let mut remaining = msg;
|
let mut remaining = line;
|
||||||
while !remaining.is_empty() {
|
// 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 {
|
let split_at = if remaining.len() <= max_msg {
|
||||||
remaining.len()
|
remaining.len()
|
||||||
} else {
|
} else {
|
||||||
// Find last char boundary at or before max_msg
|
// Find last char boundary at or before max_msg.
|
||||||
let mut i = max_msg;
|
let mut i = max_msg;
|
||||||
while i > 0 && !remaining.is_char_boundary(i) { i -= 1; }
|
while i > 0 && !remaining.is_char_boundary(i) { i -= 1; }
|
||||||
// To avoid splitting mid-word, see if there was a space recently
|
// 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;
|
let mut j = i;
|
||||||
while j > 1 && j > i-10 && remaining.as_bytes()[j] != b' ' { j -= 1; }
|
while j > 0 && (i - j) < lookback && bytes[j - 1] != b' ' {
|
||||||
if remaining.as_bytes()[j] == b' ' { j }
|
j -= 1;
|
||||||
else if i == 0 { max_msg } else { i }
|
}
|
||||||
|
if j > 0 && bytes[j - 1] == b' ' { j } else { i }
|
||||||
};
|
};
|
||||||
let (chunk, rest) = remaining.split_at(split_at);
|
let (chunk, rest) = remaining.split_at(split_at);
|
||||||
self.send_raw(&format!("PRIVMSG {target} :{chunk}")).await?;
|
self.send_raw(&format!("PRIVMSG {target} :{chunk}")).await?;
|
||||||
remaining = rest;
|
remaining = rest;
|
||||||
|
if remaining.is_empty() { break; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue