Skip to main content

rocketmq_admin_core/cli/
validators.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! CLI input validators
16//!
17//! Provides validation for command-line arguments
18
19use crate::core::RocketMQError;
20use crate::core::RocketMQResult;
21use crate::core::ToolsError;
22
23/// Validate NameServer address format
24///
25/// # Format
26/// Single: `192.168.0.1:9876`
27/// Multiple: `192.168.0.1:9876;192.168.0.2:9876`
28pub fn validate_namesrv_addr(addr: &str) -> RocketMQResult<()> {
29    if addr.is_empty() {
30        return Err(RocketMQError::Tools(ToolsError::ValidationError {
31            field: "namesrv_addr".to_string(),
32            reason: "NameServer address cannot be empty".to_string(),
33        }));
34    }
35
36    // Split by semicolon for multiple addresses
37    for single_addr in addr.split(';').map(str::trim).filter(|s| !s.is_empty()) {
38        // Check format: host:port
39        let parts: Vec<&str> = single_addr.split(':').collect();
40
41        if parts.len() != 2 {
42            return Err(RocketMQError::Tools(ToolsError::ValidationError {
43                field: "namesrv_addr".to_string(),
44                reason: format!("Invalid format '{single_addr}', expected 'host:port'"),
45            }));
46        }
47
48        // Validate port is a number
49        parts[1].parse::<u16>().map_err(|_| {
50            RocketMQError::Tools(ToolsError::ValidationError {
51                field: "namesrv_addr".to_string(),
52                reason: format!("Invalid port '{}' in address '{single_addr}'", parts[1]),
53            })
54        })?;
55    }
56
57    Ok(())
58}
59
60/// Validate topic name
61pub fn validate_topic_name(topic: &str) -> RocketMQResult<()> {
62    if topic.is_empty() {
63        return Err(RocketMQError::Tools(ToolsError::ValidationError {
64            field: "topic".to_string(),
65            reason: "Topic name cannot be empty".to_string(),
66        }));
67    }
68
69    // Check length
70    if topic.len() > 127 {
71        return Err(RocketMQError::Tools(ToolsError::ValidationError {
72            field: "topic".to_string(),
73            reason: format!("Name '{topic}' exceeds maximum length of 127 characters"),
74        }));
75    }
76
77    // Check for invalid characters
78    const INVALID_CHARS: &[char] = &['/', '\\', '|', '<', '>', '?', '*', '"', ':'];
79
80    if let Some(ch) = topic.chars().find(|c| INVALID_CHARS.contains(c)) {
81        return Err(RocketMQError::Tools(ToolsError::ValidationError {
82            field: "topic".to_string(),
83            reason: format!("Name '{topic}' contains invalid character '{ch}'"),
84        }));
85    }
86
87    Ok(())
88}
89
90/// Validate queue numbers
91pub fn validate_queue_nums(nums: i32, name: &str) -> RocketMQResult<()> {
92    match nums {
93        n if n <= 0 => Err(RocketMQError::Tools(ToolsError::ValidationError {
94            field: name.to_string(),
95            reason: format!("must be positive, got {nums}"),
96        })),
97        n if n > 1024 => Err(RocketMQError::Tools(ToolsError::ValidationError {
98            field: name.to_string(),
99            reason: format!("exceeds maximum value of 1024, got {nums}"),
100        })),
101        _ => Ok(()),
102    }
103}
104
105/// Validate permission value
106pub fn validate_perm(perm: i32) -> RocketMQResult<()> {
107    // Valid permission values: 2 (read), 4 (write), 6 (read+write)
108    match perm {
109        2 | 4 | 6 => Ok(()),
110        _ => Err(RocketMQError::Tools(ToolsError::ValidationError {
111            field: "perm".to_string(),
112            reason: format!("Invalid value {perm}, valid values are: 2 (read), 4 (write), 6 (read+write)"),
113        })),
114    }
115}
116
117/// Validate broker name
118pub fn validate_broker_name(name: &str) -> RocketMQResult<()> {
119    match name {
120        "" => Err(RocketMQError::Tools(ToolsError::ValidationError {
121            field: "broker_name".to_string(),
122            reason: "Broker name cannot be empty".to_string(),
123        })),
124        n if n.len() > 127 => Err(RocketMQError::Tools(ToolsError::ValidationError {
125            field: "broker_name".to_string(),
126            reason: format!("Name '{name}' exceeds maximum length of 127 characters"),
127        })),
128        _ => Ok(()),
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_validate_namesrv_addr() {
138        // Valid single address
139        assert!(validate_namesrv_addr("192.168.0.1:9876").is_ok());
140
141        // Valid multiple addresses
142        assert!(validate_namesrv_addr("192.168.0.1:9876;192.168.0.2:9876").is_ok());
143
144        // Invalid: empty
145        assert!(validate_namesrv_addr("").is_err());
146
147        // Invalid: no port
148        assert!(validate_namesrv_addr("192.168.0.1").is_err());
149
150        // Invalid: invalid port
151        assert!(validate_namesrv_addr("192.168.0.1:abc").is_err());
152    }
153
154    #[test]
155    fn test_validate_topic_name() {
156        // Valid
157        assert!(validate_topic_name("test_topic").is_ok());
158        assert!(validate_topic_name("TopicTest").is_ok());
159
160        // Invalid: empty
161        assert!(validate_topic_name("").is_err());
162
163        // Invalid: too long
164        assert!(validate_topic_name(&"a".repeat(128)).is_err());
165
166        // Invalid: invalid characters
167        assert!(validate_topic_name("topic/name").is_err());
168        assert!(validate_topic_name("topic\\name").is_err());
169    }
170
171    #[test]
172    fn test_validate_queue_nums() {
173        // Valid
174        assert!(validate_queue_nums(8, "read_queue_nums").is_ok());
175
176        // Invalid: negative
177        assert!(validate_queue_nums(-1, "read_queue_nums").is_err());
178
179        // Invalid: zero
180        assert!(validate_queue_nums(0, "read_queue_nums").is_err());
181
182        // Invalid: too large
183        assert!(validate_queue_nums(2000, "read_queue_nums").is_err());
184    }
185
186    #[test]
187    fn test_validate_perm() {
188        // Valid permissions
189        assert!(validate_perm(2).is_ok());
190        assert!(validate_perm(4).is_ok());
191        assert!(validate_perm(6).is_ok());
192
193        // Invalid permissions
194        assert!(validate_perm(0).is_err());
195        assert!(validate_perm(1).is_err());
196        assert!(validate_perm(3).is_err());
197        assert!(validate_perm(8).is_err());
198    }
199
200    #[test]
201    fn test_validate_broker_name() {
202        // Valid
203        assert!(validate_broker_name("broker-a").is_ok());
204
205        // Invalid: empty
206        assert!(validate_broker_name("").is_err());
207
208        // Invalid: too long
209        assert!(validate_broker_name(&"a".repeat(128)).is_err());
210    }
211}