message: parse on space (0x20) only per RFC — trim_start() also ate tabs, so a param containing a tab (e.g. a trailing ':\t') didn't round-trip through to_wire/parse; the parser fuzz proptest found it. trim_start_matches(' ') makes tabs ordinary param content; regression case pinned

This commit is contained in:
Jean Chevronnet 2026-08-19 01:41:22 +00:00
parent 98e5cf4d20
commit 11852332dc
2 changed files with 12 additions and 5 deletions

View file

@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 19cc356a7360943bfb23bdff0f579425f8957a37395bc952950f8f2250d047a3 # shrinks to line = "0 :\t"

View file

@ -48,7 +48,7 @@ impl Message {
/// Parse one wire line. Returns `None` for an empty/garbage line. /// Parse one wire line. Returns `None` for an empty/garbage line.
pub fn parse(line: &str) -> Option<Message> { pub fn parse(line: &str) -> Option<Message> {
let mut rest = line.trim_start(); let mut rest = line.trim_start_matches(' ');
// IRCv3 message tags — keep the client-only (`+`) tags for relay, drop the rest. // IRCv3 message tags — keep the client-only (`+`) tags for relay, drop the rest.
let mut ctags = String::new(); let mut ctags = String::new();
@ -71,18 +71,18 @@ pub fn parse(line: &str) -> Option<Message> {
.find_map(|t| t.strip_prefix("batch=")) .find_map(|t| t.strip_prefix("batch="))
.map(|v| v.to_string()); .map(|v| v.to_string());
concat = tags.split(';').any(|t| t == "draft/multiline-concat"); concat = tags.split(';').any(|t| t == "draft/multiline-concat");
rest = r.trim_start(); rest = r.trim_start_matches(' ');
} }
let mut source = None; let mut source = None;
if let Some(after_colon) = rest.strip_prefix(':') { if let Some(after_colon) = rest.strip_prefix(':') {
let (src, r) = after_colon.split_once(' ')?; let (src, r) = after_colon.split_once(' ')?;
source = Some(src.to_string()); source = Some(src.to_string());
rest = r.trim_start(); rest = r.trim_start_matches(' ');
} }
let (cmd, mut rest) = match rest.split_once(' ') { let (cmd, mut rest) = match rest.split_once(' ') {
Some((c, r)) => (c, r.trim_start()), Some((c, r)) => (c, r.trim_start_matches(' ')),
None => (rest, ""), None => (rest, ""),
}; };
if cmd.is_empty() { if cmd.is_empty() {
@ -98,7 +98,7 @@ pub fn parse(line: &str) -> Option<Message> {
match rest.split_once(' ') { match rest.split_once(' ') {
Some((p, r)) => { Some((p, r)) => {
params.push(p.to_string()); params.push(p.to_string());
rest = r.trim_start(); rest = r.trim_start_matches(' ');
} }
None => { None => {
params.push(rest.to_string()); params.push(rest.to_string());