1use std::collections::BTreeMap;
16use std::collections::HashMap;
17
18use cheetah_string::CheetahString;
19use serde::Deserialize;
20use serde::Serialize;
21
22use crate::protocol::route::route_data_view::BrokerData;
23use crate::protocol::route::route_data_view::QueueData;
24use crate::protocol::static_topic::topic_queue_mapping_info::TopicQueueMappingInfo;
25use crate::protocol::RemotingDeserializable;
26use crate::protocol::RemotingSerializable;
27
28#[derive(Debug, Serialize, Deserialize, Clone, Default, Eq, PartialEq)]
29pub struct TopicRouteData {
30 #[serde(rename = "orderTopicConf")]
31 pub order_topic_conf: Option<CheetahString>,
32 #[serde(rename = "queueDatas")]
33 pub queue_datas: Vec<QueueData>,
34 #[serde(rename = "brokerDatas")]
35 pub broker_datas: Vec<BrokerData>,
36 #[serde(rename = "filterServerTable")]
37 pub filter_server_table: HashMap<CheetahString, Vec<CheetahString>>,
38 #[serde(rename = "topicQueueMappingInfo", alias = "topicQueueMappingByBroker")]
39 pub topic_queue_mapping_by_broker: Option<HashMap<CheetahString, TopicQueueMappingInfo>>,
40}
41
42impl TopicRouteData {
43 pub fn topic_route_data_changed(&self, old_data: Option<&TopicRouteData>) -> bool {
44 let Some(old_data) = old_data else { return true };
45 let mut now = TopicRouteData::from_existing(self);
46 let mut old = TopicRouteData::from_existing(old_data);
47 now.queue_datas.sort();
48 now.broker_datas.sort();
49 old.queue_datas.sort();
50 old.broker_datas.sort();
51 now != old
52 }
53
54 pub fn new() -> Self {
55 TopicRouteData {
56 order_topic_conf: None,
57 queue_datas: Vec::new(),
58 broker_datas: Vec::new(),
59 filter_server_table: HashMap::new(),
60 topic_queue_mapping_by_broker: None,
61 }
62 }
63
64 pub fn from_existing(topic_route_data: &TopicRouteData) -> Self {
65 TopicRouteData {
66 order_topic_conf: topic_route_data.order_topic_conf.clone(),
67 queue_datas: topic_route_data.queue_datas.clone(),
68 broker_datas: topic_route_data.broker_datas.clone(),
69 filter_server_table: topic_route_data.filter_server_table.clone(),
70 topic_queue_mapping_by_broker: topic_route_data.topic_queue_mapping_by_broker.clone(),
71 }
72 }
73
74 pub fn encode_standard_json(&self) -> rocketmq_error::Result<Vec<u8>> {
79 StandardJsonTopicRouteData::from(self).encode()
80 }
81
82 pub fn decode(bytes: &[u8]) -> rocketmq_error::Result<Self> {
83 match <Self as RemotingDeserializable>::decode(bytes) {
84 Ok(route_data) => Ok(route_data),
85 Err(error) => {
86 let Ok(raw_body) = std::str::from_utf8(bytes) else {
87 return Err(error);
88 };
89 let Some(normalized_body) = quote_unquoted_numeric_object_keys(raw_body) else {
90 return Err(error);
91 };
92 <Self as RemotingDeserializable>::decode_str(&normalized_body)
93 }
94 }
95 }
96}
97
98fn quote_unquoted_numeric_object_keys(input: &str) -> Option<String> {
99 let bytes = input.as_bytes();
100 let mut output = String::with_capacity(input.len() + 8);
101 let mut last_copied = 0;
102 let mut index = 0;
103 let mut in_string = false;
104 let mut escaped = false;
105 let mut changed = false;
106
107 while index < bytes.len() {
108 let byte = bytes[index];
109 if in_string {
110 if escaped {
111 escaped = false;
112 } else if byte == b'\\' {
113 escaped = true;
114 } else if byte == b'"' {
115 in_string = false;
116 }
117 index += 1;
118 continue;
119 }
120
121 if byte == b'"' {
122 in_string = true;
123 index += 1;
124 continue;
125 }
126
127 if byte == b'{' || byte == b',' {
128 let mut key_start = index + 1;
129 while key_start < bytes.len() && bytes[key_start].is_ascii_whitespace() {
130 key_start += 1;
131 }
132
133 let mut digit_start = key_start;
134 if digit_start < bytes.len() && bytes[digit_start] == b'-' {
135 digit_start += 1;
136 }
137
138 if digit_start < bytes.len() && bytes[digit_start].is_ascii_digit() {
139 let mut digit_end = digit_start + 1;
140 while digit_end < bytes.len() && bytes[digit_end].is_ascii_digit() {
141 digit_end += 1;
142 }
143
144 let mut colon_index = digit_end;
145 while colon_index < bytes.len() && bytes[colon_index].is_ascii_whitespace() {
146 colon_index += 1;
147 }
148
149 if colon_index < bytes.len() && bytes[colon_index] == b':' {
150 output.push_str(&input[last_copied..key_start]);
151 output.push('"');
152 output.push_str(&input[key_start..digit_end]);
153 output.push('"');
154 last_copied = digit_end;
155 changed = true;
156 index = digit_end;
157 continue;
158 }
159 }
160 }
161
162 index += 1;
163 }
164
165 if changed {
166 output.push_str(&input[last_copied..]);
167 Some(output)
168 } else {
169 None
170 }
171}
172
173#[derive(Serialize)]
174struct StandardJsonTopicRouteData<'a> {
175 #[serde(rename = "orderTopicConf")]
176 order_topic_conf: Option<&'a CheetahString>,
177 #[serde(rename = "queueDatas")]
178 queue_datas: &'a [QueueData],
179 #[serde(rename = "brokerDatas")]
180 broker_datas: Vec<StandardJsonBrokerData<'a>>,
181 #[serde(rename = "filterServerTable")]
182 filter_server_table: BTreeMap<String, &'a Vec<CheetahString>>,
183 #[serde(rename = "topicQueueMappingInfo")]
184 topic_queue_mapping_by_broker: Option<BTreeMap<String, StandardJsonTopicQueueMappingInfo<'a>>>,
185}
186
187#[derive(Serialize)]
188struct StandardJsonBrokerData<'a> {
189 cluster: &'a str,
190 #[serde(rename = "brokerName")]
191 broker_name: &'a CheetahString,
192 #[serde(rename = "brokerAddrs")]
193 broker_addrs: BTreeMap<String, &'a CheetahString>,
194 #[serde(rename = "zoneName")]
195 zone_name: Option<&'a CheetahString>,
196 #[serde(rename = "enableActingMaster")]
197 enable_acting_master: bool,
198}
199
200#[derive(Serialize)]
201struct StandardJsonTopicQueueMappingInfo<'a> {
202 topic: Option<&'a CheetahString>,
203 scope: Option<&'a CheetahString>,
204 #[serde(rename = "totalQueues")]
205 total_queues: i32,
206 bname: Option<&'a CheetahString>,
207 epoch: i64,
208 dirty: bool,
209 #[serde(rename = "currIdMap")]
210 curr_id_map: Option<BTreeMap<String, i32>>,
211}
212
213impl<'a> From<&'a TopicRouteData> for StandardJsonTopicRouteData<'a> {
214 fn from(value: &'a TopicRouteData) -> Self {
215 Self {
216 order_topic_conf: value.order_topic_conf.as_ref(),
217 queue_datas: &value.queue_datas,
218 broker_datas: value.broker_datas.iter().map(StandardJsonBrokerData::from).collect(),
219 filter_server_table: value
220 .filter_server_table
221 .iter()
222 .map(|(broker_addr, filter_servers)| (broker_addr.to_string(), filter_servers))
223 .collect(),
224 topic_queue_mapping_by_broker: value.topic_queue_mapping_by_broker.as_ref().map(|mapping| {
225 mapping
226 .iter()
227 .map(|(broker_name, info)| (broker_name.to_string(), StandardJsonTopicQueueMappingInfo::from(info)))
228 .collect()
229 }),
230 }
231 }
232}
233
234impl<'a> From<&'a BrokerData> for StandardJsonBrokerData<'a> {
235 fn from(value: &'a BrokerData) -> Self {
236 Self {
237 cluster: value.cluster(),
238 broker_name: value.broker_name(),
239 broker_addrs: value
240 .broker_addrs()
241 .iter()
242 .map(|(broker_id, broker_addr)| (broker_id.to_string(), broker_addr))
243 .collect(),
244 zone_name: value.zone_name(),
245 enable_acting_master: value.enable_acting_master(),
246 }
247 }
248}
249
250impl<'a> From<&'a TopicQueueMappingInfo> for StandardJsonTopicQueueMappingInfo<'a> {
251 fn from(value: &'a TopicQueueMappingInfo) -> Self {
252 Self {
253 topic: value.topic.as_ref(),
254 scope: value.scope.as_ref(),
255 total_queues: value.total_queues,
256 bname: value.bname.as_ref(),
257 epoch: value.epoch,
258 dirty: value.dirty,
259 curr_id_map: value.curr_id_map.as_ref().map(|mapping| {
260 mapping
261 .iter()
262 .map(|(queue_id, mapped_id)| (queue_id.to_string(), *mapped_id))
263 .collect()
264 }),
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use std::collections::HashMap;
272
273 use serde_json;
274
275 use super::*;
276 use crate::protocol::route::route_data_view::BrokerData;
277
278 #[test]
279 fn serde_preserves_topic_route_data() {
280 let mut filter_server_table = HashMap::new();
281 filter_server_table.insert(CheetahString::from("key"), vec![CheetahString::from("value")]);
282 let mut topic_queue_mapping_by_broker = HashMap::new();
283 topic_queue_mapping_by_broker.insert(CheetahString::from("broker"), TopicQueueMappingInfo::default());
284 let topic_route_data = TopicRouteData {
285 order_topic_conf: Some(CheetahString::from("conf")),
286 queue_datas: vec![QueueData::default()],
287 broker_datas: vec![BrokerData::default()],
288 filter_server_table,
289 topic_queue_mapping_by_broker: Some(topic_queue_mapping_by_broker),
290 };
291 let serialized = serde_json::to_string(&topic_route_data).unwrap();
292 assert!(serialized.contains("\"orderTopicConf\":\"conf\""));
293 assert!(serialized.contains("\"queueDatas\":["));
294 assert!(serialized.contains("\"brokerDatas\":["));
295 assert!(serialized.contains("\"filterServerTable\":{\"key\":[\"value\"]}"));
296 assert!(serialized.contains("\"topicQueueMappingInfo\":{\"broker\":{"));
297
298 let decoded = TopicRouteData::decode(serialized.as_bytes()).unwrap();
299 assert_eq!(decoded, topic_route_data);
300 }
301
302 #[test]
303 fn decode_java_fastjson_topic_route_data() {
304 let java_body = br#"{"brokerDatas":[{"brokerAddrs":{0:"127.0.0.1:10911"},"brokerName":"interopBroker","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"interopBroker","perm":6,"readQueueNums":4,"topicSysFlag":0,"writeQueueNums":4}],"topicQueueMappingByBroker":{"interopBroker":{"topic":"TopicA","scope":"ScopeA","totalQueues":4,"bname":"interopBroker","epoch":1,"dirty":false,"currIdMap":{0:0}}}}"#;
305
306 let decoded = TopicRouteData::decode(java_body).expect("decode Java fastjson route data");
307
308 assert_eq!(decoded.queue_datas.len(), 1);
309 assert_eq!(decoded.queue_datas[0].broker_name, CheetahString::from("interopBroker"));
310 assert_eq!(decoded.broker_datas.len(), 1);
311 assert_eq!(
312 decoded.broker_datas[0].broker_addrs().get(&0),
313 Some(&CheetahString::from("127.0.0.1:10911"))
314 );
315 let mapping = decoded
316 .topic_queue_mapping_by_broker
317 .as_ref()
318 .and_then(|items| items.get(&CheetahString::from("interopBroker")))
319 .expect("topicQueueMappingByBroker alias should decode");
320 assert_eq!(mapping.curr_id_map.as_ref().and_then(|items| items.get(&0)), Some(&0));
321 }
322
323 #[test]
324 fn encode_standard_json_sorts_nested_map_keys() {
325 let mut broker_addrs = HashMap::new();
326 broker_addrs.insert(10, CheetahString::from("10.0.0.10:10911"));
327 broker_addrs.insert(2, CheetahString::from("10.0.0.2:10911"));
328
329 let mut filter_server_table = HashMap::new();
330 filter_server_table.insert(CheetahString::from("z-broker"), vec![CheetahString::from("fs-z")]);
331 filter_server_table.insert(CheetahString::from("a-broker"), vec![CheetahString::from("fs-a")]);
332
333 let mut curr_id_map = HashMap::new();
334 curr_id_map.insert(8, 80);
335 curr_id_map.insert(1, 10);
336
337 let mut topic_queue_mapping_by_broker = HashMap::new();
338 topic_queue_mapping_by_broker.insert(
339 CheetahString::from("mapping-broker"),
340 TopicQueueMappingInfo {
341 topic: Some(CheetahString::from("TestTopic")),
342 scope: Some(CheetahString::from("scope-a")),
343 total_queues: 8,
344 bname: Some(CheetahString::from("broker-a")),
345 epoch: 3,
346 dirty: false,
347 curr_id_map: Some(curr_id_map),
348 },
349 );
350
351 let topic_route_data = TopicRouteData {
352 order_topic_conf: Some(CheetahString::from("order-conf")),
353 queue_datas: vec![QueueData::new(CheetahString::from("broker-a"), 4, 4, 6, 0)],
354 broker_datas: vec![BrokerData::new(
355 CheetahString::from("cluster-a"),
356 CheetahString::from("broker-a"),
357 broker_addrs,
358 Some(CheetahString::from("zone-a")),
359 )],
360 filter_server_table,
361 topic_queue_mapping_by_broker: Some(topic_queue_mapping_by_broker),
362 };
363
364 let encoded = String::from_utf8(topic_route_data.encode_standard_json().unwrap()).unwrap();
365 let broker_addrs_index = encoded.find("\"brokerAddrs\":{\"10\"").unwrap();
366 let broker_addrs_second_index = encoded.find("\"2\":\"10.0.0.2:10911\"").unwrap();
367 let filter_server_index = encoded.find("\"filterServerTable\":{\"a-broker\"").unwrap();
368 let filter_server_second_index = encoded.find("\"z-broker\":[\"fs-z\"]").unwrap();
369 let curr_id_map_index = encoded.find("\"currIdMap\":{\"1\":10").unwrap();
370 let curr_id_map_second_index = encoded.find("\"8\":80").unwrap();
371
372 assert!(broker_addrs_index < broker_addrs_second_index);
373 assert!(filter_server_index < filter_server_second_index);
374 assert!(curr_id_map_index < curr_id_map_second_index);
375 }
376
377 #[test]
378 fn topic_route_data_changed_ignores_broker_and_queue_order() {
379 let queue = |name: &str, count| QueueData::new(name.into(), count, count, 6, 0);
380 let broker = |name: &str| BrokerData::new("cluster".into(), name.into(), HashMap::new(), None);
381 let route_data = TopicRouteData {
382 queue_datas: vec![queue("broker-b", 8), queue("broker-a", 4)],
383 broker_datas: vec![broker("broker-b"), broker("broker-a")],
384 ..Default::default()
385 };
386
387 assert!(route_data.topic_route_data_changed(None));
388 assert!(!route_data.topic_route_data_changed(Some(&route_data)));
389
390 let mut reordered = TopicRouteData::from_existing(&route_data);
391 reordered.queue_datas.reverse();
392 reordered.broker_datas.reverse();
393 assert!(!route_data.topic_route_data_changed(Some(&reordered)));
394
395 let mut changed = TopicRouteData::from_existing(&route_data);
396 changed.order_topic_conf = Some(CheetahString::from("changed"));
397 assert!(route_data.topic_route_data_changed(Some(&changed)));
398 }
399
400 #[test]
401 fn numeric_key_normalization_handles_signed_nested_keys_and_whitespace() {
402 let input = r#"{ 12 : { -3 : "value", 4: [1,-2,3] }, -5 : 6 }"#;
403 let normalized = quote_unquoted_numeric_object_keys(input).unwrap();
404 assert_eq!(
405 serde_json::from_str::<serde_json::Value>(&normalized).unwrap(),
406 serde_json::json!({
407 "12": {"-3": "value", "4": [1,-2,3]}, "-5": 6
408 })
409 );
410 }
411
412 #[test]
413 fn numeric_key_normalization_leaves_strings_and_numeric_values_unchanged() {
414 for input in [
415 r#"{"12": 3, "-4": [5,-6]}"#,
416 r#"{"text":"12: and \"-3:\" and \\4:","value":17}"#,
417 ] {
418 assert_eq!(quote_unquoted_numeric_object_keys(input), None);
419 }
420 let input = r#"{12:"-3: and \"4:\" and \\5:", "values":[6,-7]}"#;
421 let normalized = quote_unquoted_numeric_object_keys(input).unwrap();
422 let expected = r#"{"12":"-3: and \"4:\" and \\5:", "values":[6,-7]}"#;
423 assert_eq!(normalized, expected);
424 }
425
426 #[test]
427 fn decode_java_fastjson_accepts_negative_mapping_queue_ids() {
428 let body = br#"{"brokerDatas":[],"filterServerTable":{},"queueDatas":[],"topicQueueMappingByBroker":{"broker":{"topic":"topic","scope":"scope","totalQueues":1,"bname":"broker","epoch":1,"dirty":false,"currIdMap":{-1:2}}}}"#;
429 let decoded = TopicRouteData::decode(body).unwrap();
430 let mapping = &decoded.topic_queue_mapping_by_broker.as_ref().unwrap()["broker"];
431 assert_eq!(mapping.curr_id_map, Some(HashMap::from([(-1, 2)])));
432 }
433}