#![allow(dead_code, reason = "not public yet and only used in some features")]
pub struct MessageLengthLimiter<'a> {
pub head: Option<&'a str>,
pub body: Option<&'a str>,
pub tail: Option<&'a str>,
}
impl MessageLengthLimiter<'_> {
pub(crate) fn split_at(&mut self, max_len: usize) -> Option<String> {
let should_insert_newline_after_head =
self.head.is_some() && (self.body.is_some() || self.tail.is_some());
let should_insert_newline_after_body = self.body.is_some() && self.tail.is_some();
let msg_len = self.head.map_or(0, count_chars)
+ self.body.map_or(0, count_chars)
+ self.tail.map_or(0, count_chars)
+ usize::from(should_insert_newline_after_head)
+ usize::from(should_insert_newline_after_body);
if msg_len == 0 {
return None;
}
let next = if msg_len > max_len {
compose_long_message(&mut self.head, &mut self.body, &mut self.tail, max_len)
} else {
Some(format!(
"{}{}{}{}{}",
self.head.take().unwrap_or_default(),
should_insert_newline_after_head
.then_some("\n")
.unwrap_or_default(),
self.body.take().unwrap_or_default(),
should_insert_newline_after_body
.then_some("\n")
.unwrap_or_default(),
self.tail.take().unwrap_or_default()
))
};
assert!(
next.as_ref().is_none_or(|s| !s.is_empty()),
"We should've ensured the final composed message isn't empty but it is anyways"
);
next
}
}
fn compose_long_message(
head: &mut Option<&str>,
body: &mut Option<&str>,
tail: &mut Option<&str>,
max_len: usize,
) -> Option<String> {
if head.is_none() && body.is_none() && tail.is_none() {
return None;
}
let head_len = head.map_or(0, count_chars);
assert!(
max_len >= head_len,
"head has more characters: {head_len}, than can be fit in a msg part of max len: {max_len}"
);
let tail_len = tail.map_or(0, count_chars);
assert!(
max_len >= tail_len,
"tail has more characters: {tail_len}, than can be fit in a msg part of max len: {max_len}"
);
let mut split_part = String::with_capacity(max_len);
if let Some(head) = head.take() {
split_part.push_str(head);
}
if let Some(body_str) = body.take() {
let space_left_for_body = max_len.checked_sub(split_part.chars().count()).expect(
"only the head should've been pushed to the split and we asserted that it isn't longer than len",
);
let body_fits_till = body_str
.char_indices()
.nth(space_left_for_body)
.map_or_else(|| body_str.len(), |(idx, _)| idx);
let (body_fits_till, add_newline) = if split_part.is_empty() {
(body_fits_till, false)
} else {
(body_fits_till.saturating_sub(1), true)
};
if body_fits_till > 0 {
if add_newline {
split_part.push('\n');
}
split_part.push_str(&body_str[..body_fits_till]);
let remaining_body = &body_str[body_fits_till..];
if !remaining_body.is_empty() {
*body = Some(remaining_body);
}
} else {
*body = Some(body_str);
}
}
{
let (tail_len, add_newline) = if split_part.is_empty() {
(tail_len, false)
} else {
(tail_len + 1, true)
};
if max_len.saturating_sub(split_part.chars().count()) >= tail_len {
if let Some(tail) = tail.take() {
if add_newline {
split_part.push('\n');
}
split_part.push_str(tail);
}
}
}
{
let split_part_chars = split_part.chars().count();
assert!(
split_part_chars <= max_len,
"Returned a part with char len of {split_part_chars} when it should never be longer than {max_len}"
);
}
Some(split_part)
}
fn count_chars(s: &str) -> usize {
s.chars().count()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
const MAX_MSG_LEN: usize = 4096;
const BODY_COUNT: usize = 3;
const HEAD: &str = "HEAD";
const BODY: &str = "BODY";
const TAIL: &str = "TAIL";
impl Iterator for MessageLengthLimiter<'_> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
self.split_at(MAX_MSG_LEN)
}
}
#[test]
fn format_head_body_tail() {
const FINAL: &str = "HEAD\nBODY\nTAIL";
let mut msg = MessageLengthLimiter {
head: Some(HEAD),
body: Some(BODY),
tail: Some(TAIL),
};
assert_eq!(msg.next().as_deref(), Some(FINAL));
assert_eq!(msg.next(), None);
}
#[test]
fn format_head_tail() {
const FINAL: &str = "HEAD\nTAIL";
let mut msg = MessageLengthLimiter {
head: Some(HEAD),
body: None,
tail: Some(TAIL),
};
assert_eq!(msg.next().as_deref(), Some(FINAL));
assert_eq!(msg.next(), None);
}
#[test]
fn format_body_tail() {
const FINAL: &str = "BODY\nTAIL";
let mut msg = MessageLengthLimiter {
head: None,
body: Some(BODY),
tail: Some(TAIL),
};
assert_eq!(msg.next().as_deref(), Some(FINAL));
assert_eq!(msg.next(), None);
}
#[test]
fn format_head_body() {
const FINAL: &str = "HEAD\nBODY";
let mut msg = MessageLengthLimiter {
head: Some(HEAD),
body: Some(BODY),
tail: None,
};
assert_eq!(msg.next().as_deref(), Some(FINAL));
assert_eq!(msg.next(), None);
}
#[test]
fn short_body() {
const STR: &str = "Hello, World!";
let mut msg = MessageLengthLimiter {
head: None,
body: Some(STR),
tail: None,
};
assert_eq!(msg.next().as_deref(), Some(STR));
assert_eq!(msg.next(), None);
}
#[test]
fn empty_head_tail_long_body() {
let mut body = String::new();
for _ in 0..MAX_MSG_LEN * BODY_COUNT {
body.push('b');
}
let msg = MessageLengthLimiter {
head: None,
body: Some(&body),
tail: None,
};
let mut msg = msg.peekable();
assert_eq!(msg.peek().map(|s| &**s), Some(&body[..MAX_MSG_LEN]));
assert_eq!(msg.count(), BODY_COUNT);
}
#[test]
fn long_head() {
let mut head = String::new();
for _ in 0..150 {
head.push('h');
}
let mut body = String::new();
for _ in 0..MAX_MSG_LEN * BODY_COUNT {
body.push('b');
}
let msg = MessageLengthLimiter {
head: Some(&head),
body: Some(&body),
tail: None,
};
assert_eq!(msg.count(), BODY_COUNT + 1);
}
#[test]
fn with_tail_almost_fitting() {
let mut body = String::new();
for _ in 0..MAX_MSG_LEN * BODY_COUNT - 1 {
body.push('b');
}
let tail = "tt".to_owned();
let msg = MessageLengthLimiter {
head: None,
body: Some(&body),
tail: Some(&tail),
};
assert_eq!(msg.count(), BODY_COUNT + 1); }
#[test]
fn with_all_parts_of_max_len() {
let mut head = String::new();
for _ in 0..MAX_MSG_LEN {
head.push('h');
}
let mut body = String::new();
for _ in 0..MAX_MSG_LEN * BODY_COUNT {
body.push('b');
}
let mut tail = String::new();
for _ in 0..MAX_MSG_LEN {
tail.push('t');
}
let msg = MessageLengthLimiter {
head: Some(&head),
body: Some(&body),
tail: Some(&tail),
};
assert_eq!(msg.count(), BODY_COUNT + 2);
}
#[test]
#[should_panic = "head has more characters: 128, than can be fit in a msg part of max len: 127"]
fn split_at_shorter_than_head() {
const HEAD_LEN: usize = 128;
let mut head = String::new();
for _ in 0..HEAD_LEN {
head.push('h');
}
let mut msg = MessageLengthLimiter {
head: Some(&head),
body: None,
tail: None,
};
_ = msg.split_at(HEAD_LEN - 1);
}
}