use std::collections::HashMap;
use cheetah_string::CheetahString;
use rocketmq_model::common::config::TopicConfig;
use rocketmq_model::common::constant::PermName;
use rocketmq_model::common::message::MessageConst;
use rocketmq_model::common::message::MessageTrait;
use rocketmq_model::common::topic::TopicValidator;
use rocketmq_protocol::code::response_code::ResponseCode;
use crate::producer::default_mq_producer::ProducerConfig;
pub struct Validators;
impl Validators {
pub const CHARACTER_MAX_LENGTH: usize = 255;
pub const TOPIC_MAX_LENGTH: usize = 127;
pub const GROUP_MAX_LENGTH: usize = 120;
pub fn check_group(group: &str) -> crate::ClientResult<()> {
if group.trim().is_empty() {
return Err(mq_client_err!("the specified group is blank"));
}
if group.len() > Self::GROUP_MAX_LENGTH {
return Err(mq_client_err!(format!(
"the specified group[{}] is longer than group max length: {}.",
group,
Self::GROUP_MAX_LENGTH
)));
}
if TopicValidator::is_topic_or_group_illegal(group) {
return Err(mq_client_err!(format!(
"the specified group[{}] contains illegal characters, allowing only ^[%|a-zA-Z0-9_-]+$",
group
)));
}
Ok(())
}
pub fn check_message<M>(msg: Option<&M>, producer_config: &ProducerConfig) -> crate::ClientResult<()>
where
M: MessageTrait,
{
let Some(msg) = msg else {
return Err(mq_client_err!(
ResponseCode::MessageIllegal as i32,
"the message is null".to_string()
));
};
Self::check_topic(msg.topic())?;
Self::is_not_allowed_send_topic(msg.topic())?;
let Some(body) = msg.get_body() else {
return Err(mq_client_err!(
ResponseCode::MessageIllegal as i32,
"the message body is null".to_string()
));
};
let length = body.len();
if length == 0 {
return Err(mq_client_err!(
ResponseCode::MessageIllegal as i32,
"the message body length is zero".to_string()
));
}
if length > producer_config.max_message_size() as usize {
return Err(mq_client_err!(
ResponseCode::MessageIllegal as i32,
format!(
"the message body size over max value, MAX: {}",
producer_config.max_message_size()
)
));
}
let lmq_path = msg.user_property(&CheetahString::from_static_str(
MessageConst::PROPERTY_INNER_MULTI_DISPATCH,
));
if let Some(value) = lmq_path {
if value.contains(std::path::MAIN_SEPARATOR) {
return Err(mq_client_err!(
ResponseCode::MessageIllegal as i32,
format!(
"INNER_MULTI_DISPATCH {} can not contains {} character",
value,
std::path::MAIN_SEPARATOR
)
));
}
}
Ok(())
}
pub fn check_topic(topic: &str) -> crate::ClientResult<()> {
if topic.trim().is_empty() {
return Err(mq_client_err!("The specified topic is blank"));
}
if topic.len() > Self::TOPIC_MAX_LENGTH {
return Err(mq_client_err!(format!(
"The specified topic is longer than topic max length {}.",
Self::TOPIC_MAX_LENGTH
)));
}
if TopicValidator::is_topic_or_group_illegal(topic) {
return Err(mq_client_err!(format!(
"The specified topic[{}] contains illegal characters, allowing only ^[%|a-zA-Z0-9_-]+$",
topic
)));
}
Ok(())
}
pub fn is_system_topic(topic: &str) -> crate::ClientResult<()> {
if TopicValidator::is_system_topic(topic) {
return Err(mq_client_err!(format!(
"The topic[{}] is conflict with system topic.",
topic
)));
}
Ok(())
}
pub fn is_not_allowed_send_topic(topic: &str) -> crate::ClientResult<()> {
if TopicValidator::is_not_allowed_send_topic(topic) {
return Err(mq_client_err!(format!(
"Sending message to topic[{}] is forbidden.",
topic
)));
}
Ok(())
}
pub fn check_topic_config(topic_config: &TopicConfig) -> crate::ClientResult<()> {
if !PermName::is_valid(topic_config.perm) {
return Err(mq_client_err!(
ResponseCode::NoPermission as i32,
format!("topicPermission value: {} is invalid.", topic_config.perm)
));
}
Ok(())
}
pub fn check_broker_config(broker_config: &HashMap<String, String>) -> crate::ClientResult<()> {
if let Some(broker_permission) = broker_config.get("brokerPermission") {
if !PermName::is_valid_str(broker_permission) {
return Err(mq_client_err!(format!(
"brokerPermission value: {} is invalid.",
broker_permission
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::test_support::error_assertions::client_exception;
use std::collections::HashMap;
use bytes::Bytes;
use rocketmq_model::common::config::TopicConfig;
use rocketmq_model::common::message::message_accessor::MessageAccessor;
use rocketmq_model::common::message::message_single::Message;
use super::*;
#[test]
fn check_group_blank_group() {
let result = Validators::check_group("");
assert!(result.is_err());
}
#[test]
fn check_message_accepts_exact_body_limit_and_rejects_one_byte_over() {
let producer_config = ProducerConfig::default();
let max_message_size = producer_config.max_message_size() as usize;
let exact = Message::builder()
.topic("TopicTest")
.body(Bytes::from(vec![0_u8; max_message_size]))
.build_unchecked();
let oversized = Message::builder()
.topic("TopicTest")
.body(Bytes::from(vec![0_u8; max_message_size + 1]))
.build_unchecked();
assert!(Validators::check_message(Some(&exact), &producer_config).is_ok());
assert!(Validators::check_message(Some(&oversized), &producer_config).is_err());
}
#[test]
fn check_message_rejects_absent_message() {
let producer_config = ProducerConfig::default();
let error =
Validators::check_message(None::<&Message>, &producer_config).expect_err("absent message is rejected");
assert_eq!(
client_exception(&error).response_code(),
ResponseCode::MessageIllegal as i32
);
}
#[test]
fn check_message_rejects_missing_and_empty_body_with_the_same_code() {
let producer_config = ProducerConfig::default();
let no_body = Message::builder().topic("TopicTest").build_unchecked();
let error = Validators::check_message(Some(&no_body), &producer_config).expect_err("missing body is rejected");
assert_eq!(
client_exception(&error).response_code(),
ResponseCode::MessageIllegal as i32
);
let empty_body = Message::builder()
.topic("TopicTest")
.body(Bytes::new())
.build_unchecked();
let error = Validators::check_message(Some(&empty_body), &producer_config).expect_err("empty body is rejected");
assert_eq!(
client_exception(&error).response_code(),
ResponseCode::MessageIllegal as i32
);
}
#[test]
fn check_message_rejects_multi_dispatch_property_containing_path_separator() {
let producer_config = ProducerConfig::default();
let mut msg = Message::builder()
.topic("TopicTest")
.body(Bytes::from_static(b"body"))
.build_unchecked();
let separator = std::path::MAIN_SEPARATOR;
MessageAccessor::put_property(
&mut msg,
CheetahString::from_static_str(MessageConst::PROPERTY_INNER_MULTI_DISPATCH),
CheetahString::from_string(format!("queue-a{separator}queue-b")),
);
let error = Validators::check_message(Some(&msg), &producer_config)
.expect_err("multi-dispatch value containing the path separator is rejected");
assert_eq!(
client_exception(&error).response_code(),
ResponseCode::MessageIllegal as i32
);
}
#[test]
fn check_message_accepts_multi_dispatch_property_without_separator_or_absent() {
let producer_config = ProducerConfig::default();
let plain = Message::builder()
.topic("TopicTest")
.body(Bytes::from_static(b"body"))
.build_unchecked();
assert!(Validators::check_message(Some(&plain), &producer_config).is_ok());
let mut with_property = Message::builder()
.topic("TopicTest")
.body(Bytes::from_static(b"body"))
.build_unchecked();
MessageAccessor::put_property(
&mut with_property,
CheetahString::from_static_str(MessageConst::PROPERTY_INNER_MULTI_DISPATCH),
CheetahString::from_static_str("queue-a,queue-b"),
);
assert!(Validators::check_message(Some(&with_property), &producer_config).is_ok());
}
#[test]
fn check_group_long_group() {
let long_group = "a".repeat(256);
let result = Validators::check_group(&long_group);
assert!(result.is_err());
}
#[test]
fn check_group_matches_java_max_length() {
let max_group = "a".repeat(Validators::GROUP_MAX_LENGTH);
assert!(Validators::check_group(&max_group).is_ok());
let too_long_group = "a".repeat(Validators::GROUP_MAX_LENGTH + 1);
let result = Validators::check_group(&too_long_group);
assert!(result.is_err());
assert!(client_exception(&result.unwrap_err())
.to_string()
.contains("longer than group max length: 120."));
}
#[test]
fn check_group_illegal_characters() {
let result = Validators::check_group("illegal@group");
assert!(result.is_err());
}
#[test]
fn check_group_valid_group() {
let result = Validators::check_group("valid_group");
assert!(result.is_ok());
}
#[test]
fn check_topic_blank_topic() {
let result = Validators::check_topic("");
assert!(result.is_err());
}
#[test]
fn check_topic_long_topic() {
let long_topic = "a".repeat(128);
let result = Validators::check_topic(&long_topic);
assert!(result.is_err());
}
#[test]
fn check_topic_illegal_characters() {
let result = Validators::check_topic("illegal@topic");
assert!(result.is_err());
}
#[test]
fn check_topic_valid_topic() {
let result = Validators::check_topic("valid_topic");
assert!(result.is_ok());
}
#[test]
fn check_topic_config_invalid_permission() {
let topic_config = TopicConfig {
perm: 999,
..Default::default()
};
let result = Validators::check_topic_config(&topic_config);
assert!(result.is_err());
}
#[test]
fn check_topic_config_valid_permission() {
let topic_config = TopicConfig {
perm: 6,
..Default::default()
};
let result = Validators::check_topic_config(&topic_config);
assert!(result.is_ok());
}
#[test]
fn check_broker_config_invalid_permission() {
let mut broker_config = HashMap::new();
broker_config.insert("brokerPermission".to_string(), "999".to_string());
let result = Validators::check_broker_config(&broker_config);
assert!(result.is_err());
}
#[test]
fn check_broker_config_rejects_non_numeric_permission_without_panic() {
let mut broker_config = HashMap::new();
broker_config.insert("brokerPermission".to_string(), "abc".to_string());
let result = Validators::check_broker_config(&broker_config);
assert!(result.is_err());
}
#[test]
fn check_broker_config_valid_permission() {
let mut broker_config = HashMap::new();
broker_config.insert("brokerPermission".to_string(), "6".to_string());
let result = Validators::check_broker_config(&broker_config);
assert!(result.is_ok());
}
}