Skip to main content

rocketmq_protocol/protocol/
rocketmq_serializable.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
15use std::collections::HashMap;
16use std::str;
17
18use bytes::Buf;
19use bytes::BufMut;
20use bytes::Bytes;
21use bytes::BytesMut;
22use cheetah_string::CheetahString;
23
24use crate::protocol::command_custom_header::CommandCustomHeader;
25use crate::protocol::command_custom_header::HeaderEncodeCapability;
26use crate::protocol::header_codec::BinaryHeaderFields;
27use crate::protocol::header_field_merge::has_custom_ext_collision;
28use crate::protocol::header_field_merge::merge_header_and_dynamic;
29use crate::protocol::remoting_command::RemotingCommand;
30use crate::protocol::LanguageCode;
31use crate::ProtocolContractViolation;
32
33const MAP_ENTRY_ENCODED_BYTES_ESTIMATE: usize = 16;
34const MIN_MAP_CAPACITY: usize = 4;
35const MAX_MAP_CAPACITY: usize = 1024;
36
37fn decoding_error(required: usize, available: usize) -> rocketmq_error::Error {
38    crate::error::serialization_decode_failed("binary", format!("required {required} bytes, got {available}"))
39}
40
41fn trailing_header_error(remaining: usize) -> rocketmq_error::Error {
42    crate::error::serialization_decode_failed(
43        "binary",
44        format!("ROCKETMQ header has {remaining} trailing bytes after extension fields"),
45    )
46}
47
48enum SortedExtFields<'a> {
49    Direct(std::collections::hash_map::Iter<'a, CheetahString, CheetahString>),
50    Sorted(std::vec::IntoIter<(&'a CheetahString, &'a CheetahString)>),
51}
52
53impl<'a> Iterator for SortedExtFields<'a> {
54    type Item = (&'a CheetahString, &'a CheetahString);
55
56    fn next(&mut self) -> Option<Self::Item> {
57        match self {
58            Self::Direct(entries) => entries.find(|(key, _)| !key.is_empty()),
59            Self::Sorted(entries) => entries.next(),
60        }
61    }
62}
63
64fn sorted_ext_fields(map: &HashMap<CheetahString, CheetahString>) -> SortedExtFields<'_> {
65    if map.len() <= 1 {
66        return SortedExtFields::Direct(map.iter());
67    }
68    let mut entries = map.iter().filter(|(key, _)| !key.is_empty()).collect::<Vec<_>>();
69    entries.sort_unstable_by(|(left, _), (right, _)| left.as_str().cmp(right.as_str()));
70    SortedExtFields::Sorted(entries.into_iter())
71}
72
73pub struct RocketMQSerializable;
74
75impl RocketMQSerializable {
76    pub(crate) const INITIAL_ENCODE_CAPACITY: usize = 256;
77
78    #[inline]
79    fn estimated_map_capacity(encoded_len: usize) -> usize {
80        (encoded_len / MAP_ENTRY_ENCODED_BYTES_ESTIMATE).clamp(MIN_MAP_CAPACITY, MAX_MAP_CAPACITY)
81    }
82
83    /// Optimized string write with inline hint for better performance
84    #[inline]
85    pub fn write_str(buf: &mut BytesMut, use_short_length: bool, s: &str) -> usize {
86        let bytes = s.as_bytes();
87        let len = bytes.len();
88
89        let length_size = if use_short_length {
90            buf.put_u16(len as u16);
91            2
92        } else {
93            buf.put_u32(len as u32);
94            4
95        };
96
97        buf.put_slice(bytes); // Use put_slice for better performance
98        length_size + len
99    }
100
101    /// Optimized string read with enhanced boundary checks
102    #[inline]
103    pub fn read_str(
104        buf: &mut BytesMut,
105        use_short_length: bool,
106        limit: usize,
107    ) -> rocketmq_error::Result<Option<CheetahString>> {
108        // Read length prefix
109        let len = if use_short_length {
110            if buf.remaining() < 2 {
111                return Err(decoding_error(2, buf.remaining()));
112            }
113            buf.get_u16() as usize
114        } else {
115            if buf.remaining() < 4 {
116                return Err(decoding_error(4, buf.remaining()));
117            }
118            buf.get_u32() as usize
119        };
120
121        // Empty string
122        if len == 0 {
123            return Ok(None);
124        }
125
126        // Boundary check
127        if len > limit {
128            return Err(decoding_error(len, limit));
129        }
130
131        // Ensure buffer has enough data
132        if buf.remaining() < len {
133            return Err(decoding_error(len, buf.remaining()));
134        }
135
136        // Checked UTF-8 decode with CheetahString storage optimization
137        let bytes = buf.split_to(len).freeze();
138        let value = CheetahString::try_copy_from_bytes(bytes)
139            .map_err(|error| crate::error::serialization_source("decode", "binary", error.into_parts().1))?;
140        Ok(Some(value))
141    }
142
143    /// Optimized ROCKETMQ protocol encoding with reduced allocations
144    #[inline]
145    pub fn rocketmq_protocol_encode(cmd: &mut RemotingCommand, buf: &mut BytesMut) -> usize {
146        let checkpoint = buf.len();
147        match Self::try_rocketmq_protocol_encode(cmd, buf) {
148            Ok(encoded) => encoded,
149            Err(_) => {
150                buf.truncate(checkpoint);
151                0
152            }
153        }
154    }
155
156    /// Fallible ROCKETMQ protocol encoding with reduced allocations.
157    ///
158    /// # Errors
159    ///
160    /// Returns the direct custom-header encoding failure when the selected
161    /// header cannot be represented in the ROCKETMQ extension-field payload.
162    #[inline]
163    pub fn try_rocketmq_protocol_encode(
164        cmd: &RemotingCommand,
165        buf: &mut BytesMut,
166    ) -> Result<usize, ProtocolContractViolation> {
167        Self::try_rocketmq_protocol_encode_with_capability(cmd, buf, cmd.custom_header_encode_capability())
168    }
169
170    pub(crate) fn try_rocketmq_protocol_encode_with_capability(
171        cmd: &RemotingCommand,
172        buf: &mut BytesMut,
173        capability: HeaderEncodeCapability,
174    ) -> Result<usize, ProtocolContractViolation> {
175        // A bounded initial allocation is cheaper than walking every typed and
176        // dynamic field before immediately walking them again to encode. Large
177        // headers retain BytesMut's normal growth behavior.
178        buf.reserve(Self::INITIAL_ENCODE_CAPACITY);
179        let checkpoint = buf.len();
180        let result = Self::try_rocketmq_protocol_encode_inner(cmd, buf, capability);
181        if result.is_err() {
182            buf.truncate(checkpoint);
183        }
184        result
185    }
186
187    fn try_rocketmq_protocol_encode_inner(
188        cmd: &RemotingCommand,
189        buf: &mut BytesMut,
190        capability: HeaderEncodeCapability,
191    ) -> Result<usize, ProtocolContractViolation> {
192        let begin_index = buf.len();
193
194        // Write fixed-size header fields (total: 15 bytes)
195        buf.put_u16(cmd.code() as u16); // 2 bytes
196        buf.put_u8(cmd.language().get_code()); // 1 byte
197        buf.put_u16(cmd.version() as u16); // 2 bytes
198        buf.put_i32(cmd.opaque()); // 4 bytes
199        buf.put_i32(cmd.flag()); // 4 bytes
200
201        // Write remark (variable length with 4-byte prefix or 0)
202        if let Some(remark) = cmd.remark() {
203            Self::write_str(buf, false, remark.as_str());
204        } else {
205            buf.put_i32(0);
206        }
207
208        // Reserve space for ext_fields length (will be updated later)
209        let map_len_index = buf.len();
210        buf.put_i32(0);
211
212        // Keep the common direct path map-free. Semantic overlaps use the
213        // single authoritative typed/dynamic merge contract.
214        match cmd.command_custom_header_ref() {
215            Some(header)
216                if capability == HeaderEncodeCapability::DirectBinary
217                    && !has_custom_ext_collision(header, cmd.ext_fields()) =>
218            {
219                header.encode_direct_binary(buf)?;
220                if let Some(ext_fields) = cmd.ext_fields() {
221                    Self::write_ext_fields(buf, ext_fields)?;
222                }
223            }
224            Some(header) => {
225                let merged = merge_header_and_dynamic(header, cmd.ext_fields())?;
226                Self::write_ext_fields(buf, &merged)?;
227            }
228            None => {
229                if let Some(ext_fields) = cmd.ext_fields() {
230                    Self::write_ext_fields(buf, ext_fields)?;
231                }
232            }
233        }
234
235        // Update ext_fields length in-place
236        let current_length = buf.len();
237        let ext_fields_length = Self::checked_ext_fields_length(current_length - map_len_index - 4)?;
238        buf[map_len_index..map_len_index + 4].copy_from_slice(&ext_fields_length.to_be_bytes());
239
240        Ok(buf.len() - begin_index)
241    }
242
243    /// Encodes the common direct-header shape without map materialization or
244    /// collision checks. Callers must establish that no remark or dynamic
245    /// extension fields are present.
246    pub(crate) fn try_rocketmq_protocol_encode_direct(
247        cmd: &RemotingCommand,
248        header: &dyn CommandCustomHeader,
249        buf: &mut BytesMut,
250    ) -> Result<usize, ProtocolContractViolation> {
251        const FIXED_HEADER_LENGTH: usize = 21;
252        const EXT_FIELDS_LENGTH_OFFSET: usize = 17;
253
254        let begin_index = buf.len();
255        let mut fixed = [0u8; FIXED_HEADER_LENGTH];
256        fixed[..2].copy_from_slice(&(cmd.code() as u16).to_be_bytes());
257        fixed[2] = cmd.language().get_code();
258        fixed[3..5].copy_from_slice(&(cmd.version() as u16).to_be_bytes());
259        fixed[5..9].copy_from_slice(&cmd.opaque().to_be_bytes());
260        fixed[9..13].copy_from_slice(&cmd.flag().to_be_bytes());
261        buf.extend_from_slice(&fixed);
262
263        header.encode_direct_binary(buf)?;
264
265        let ext_fields_length = Self::checked_ext_fields_length(buf.len() - begin_index - FIXED_HEADER_LENGTH)?;
266        let map_len_index = begin_index + EXT_FIELDS_LENGTH_OFFSET;
267        buf[map_len_index..map_len_index + 4].copy_from_slice(&ext_fields_length.to_be_bytes());
268        Ok(buf.len() - begin_index)
269    }
270
271    #[inline]
272    fn write_ext_fields(
273        buf: &mut BytesMut,
274        fields: &HashMap<CheetahString, CheetahString>,
275    ) -> Result<(), ProtocolContractViolation> {
276        for (key, value) in sorted_ext_fields(fields) {
277            let key_length = Self::checked_dynamic_key_length(key.len())?;
278            let value_length = Self::checked_dynamic_value_length(value.len())?;
279            buf.put_u16(key_length);
280            buf.put_slice(key.as_bytes());
281            buf.put_i32(value_length);
282            buf.put_slice(value.as_bytes());
283        }
284        Ok(())
285    }
286
287    #[inline]
288    fn checked_ext_fields_length(length: usize) -> Result<i32, ProtocolContractViolation> {
289        i32::try_from(length).map_err(|_| ProtocolContractViolation::ExtensionFieldsLengthOverflow)
290    }
291
292    #[inline]
293    fn checked_dynamic_key_length(length: usize) -> Result<u16, ProtocolContractViolation> {
294        i16::try_from(length)
295            .map(|length| length as u16)
296            .map_err(|_| ProtocolContractViolation::DynamicKeyLengthOverflow)
297    }
298
299    #[inline]
300    fn checked_dynamic_value_length(length: usize) -> Result<i32, ProtocolContractViolation> {
301        i32::try_from(length).map_err(|_| ProtocolContractViolation::DynamicValueLengthOverflow)
302    }
303
304    pub fn rocket_mq_protocol_encode_bytes(cmd: &RemotingCommand) -> Bytes {
305        let remark_bytes = cmd.remark().map(|remark| remark.as_bytes().to_vec());
306        let remark_len = remark_bytes.as_ref().map_or(0, |v| v.len());
307
308        let ext_fields_bytes = if let Some(ext) = cmd.get_ext_fields() {
309            Self::map_serialize(ext)
310        } else {
311            None
312        };
313        let ext_len = ext_fields_bytes.as_ref().map_or(0, |v| v.len());
314
315        let total_len = Self::cal_total_len(remark_len, ext_len);
316        let mut header_buffer = BytesMut::with_capacity(total_len);
317
318        // int code (~32767)
319        header_buffer.put_i16(cmd.code() as i16);
320
321        // LanguageCode language
322        header_buffer.put_u8(cmd.language().get_code());
323
324        // int version (~32767)
325        header_buffer.put_i16(cmd.version() as i16);
326
327        // int opaque
328        header_buffer.put_i32(cmd.opaque());
329
330        // int flag
331        header_buffer.put_i32(cmd.flag());
332
333        // String remark
334        if let Some(remark_bytes) = remark_bytes {
335            header_buffer.put_i32(remark_bytes.len() as i32);
336            header_buffer.put(remark_bytes.as_ref());
337        } else {
338            header_buffer.put_i32(0);
339        }
340
341        // HashMap<String, String> extFields
342        if let Some(ext_fields_bytes) = ext_fields_bytes {
343            header_buffer.put_i32(ext_fields_bytes.len() as i32);
344            header_buffer.put(ext_fields_bytes.as_ref());
345        } else {
346            header_buffer.put_i32(0);
347        }
348
349        header_buffer.freeze()
350    }
351
352    /// Optimized map serialization with pre-calculated capacity
353    #[inline]
354    pub fn map_serialize(map: &HashMap<CheetahString, CheetahString>) -> Option<BytesMut> {
355        if map.is_empty() {
356            return None;
357        }
358
359        // Pre-calculate total length in a single pass
360        let mut total_length = 0;
361        let mut valid_entries = 0;
362
363        for (key, value) in map.iter() {
364            if !key.is_empty() {
365                total_length += 2 + key.len() + 4 + value.len();
366                valid_entries += 1;
367            }
368        }
369
370        if valid_entries == 0 {
371            return None;
372        }
373
374        // Allocate exact capacity (avoid reallocations)
375        let mut content = BytesMut::with_capacity(total_length);
376
377        // Serialize entries in canonical key order so equivalent maps produce identical frames.
378        for (key, value) in sorted_ext_fields(map) {
379            // Write key: u16 length + bytes
380            content.put_u16(key.len() as u16);
381            content.put_slice(key.as_bytes());
382
383            // Write value: i32 length + bytes
384            content.put_i32(value.len() as i32);
385            content.put_slice(value.as_bytes());
386        }
387
388        Some(content)
389    }
390
391    pub fn cal_total_len(remark_len: usize, ext_len: usize) -> usize {
392        // int code(~32767): 2 bytes
393        // LanguageCode language: 1 byte
394        // int version(~32767): 2 bytes
395        // int opaque: 4 bytes
396        // int flag: 4 bytes
397        // String remark length: 4 bytes + actual length of remark
398        // HashMap<String, String> extFields length: 4 bytes + actual length of extFields
399
400        2   // int code
401             + 1          // LanguageCode language
402             + 2          // int version
403             + 4          // int opaque
404             + 4          // int flag
405             + 4 + remark_len   // String remark
406             + 4 + ext_len // HashMap<String, String> extFields
407    }
408
409    pub fn rocket_mq_protocol_decode(
410        header_buffer: &mut BytesMut,
411        header_len: usize,
412    ) -> rocketmq_error::Result<RemotingCommand> {
413        let available = header_buffer.remaining();
414        if available < header_len {
415            return Err(decoding_error(header_len, available));
416        }
417        if available > header_len {
418            return Err(trailing_header_error(available - header_len));
419        }
420        Self::rocket_mq_protocol_decode_bytes(header_buffer.split().freeze())
421    }
422
423    /// Decodes an immutable ROCKETMQ header without intermediate buffer splits.
424    pub(crate) fn rocket_mq_protocol_decode_bytes(header: Bytes) -> rocketmq_error::Result<RemotingCommand> {
425        const FIXED_HEADER_LEN: usize = 13;
426        const LENGTH_FIELD_LEN: usize = 4;
427
428        if header.len() < FIXED_HEADER_LEN {
429            return Err(decoding_error(FIXED_HEADER_LEN, header.len()));
430        }
431
432        let code = i16::from_be_bytes([header[0], header[1]]) as i32;
433        let language = LanguageCode::from(header[2]);
434        let version = i16::from_be_bytes([header[3], header[4]]) as i32;
435        let opaque = i32::from_be_bytes([header[5], header[6], header[7], header[8]]);
436        let flag = i32::from_be_bytes([header[9], header[10], header[11], header[12]]);
437
438        let mut cursor = FIXED_HEADER_LEN;
439        if header.len() - cursor < LENGTH_FIELD_LEN {
440            return Err(decoding_error(LENGTH_FIELD_LEN, header.len() - cursor));
441        }
442        let remark_length = u32::from_be_bytes([
443            header[cursor],
444            header[cursor + 1],
445            header[cursor + 2],
446            header[cursor + 3],
447        ]) as usize;
448        cursor += LENGTH_FIELD_LEN;
449        if remark_length > header.len() - cursor {
450            return Err(decoding_error(remark_length, header.len() - cursor));
451        }
452        let remark = if remark_length == 0 {
453            None
454        } else {
455            let end = cursor + remark_length;
456            let value = CheetahString::try_copy_from_bytes(header.slice(cursor..end))
457                .map_err(|error| crate::error::serialization_source("decode", "binary", error.into_parts().1))?;
458            cursor = end;
459            Some(value)
460        };
461
462        if header.len() - cursor < LENGTH_FIELD_LEN {
463            return Err(decoding_error(LENGTH_FIELD_LEN, header.len() - cursor));
464        }
465        let ext_fields_length = i32::from_be_bytes([
466            header[cursor],
467            header[cursor + 1],
468            header[cursor + 2],
469            header[cursor + 3],
470        ]);
471        cursor += LENGTH_FIELD_LEN;
472        let payload = if ext_fields_length > 0 {
473            let ext_fields_length = ext_fields_length as usize;
474            if ext_fields_length > header.len() - cursor {
475                return Err(decoding_error(ext_fields_length, header.len() - cursor));
476            }
477            let end = cursor + ext_fields_length;
478            let payload = header.slice(cursor..end);
479            cursor = end;
480            payload
481        } else {
482            Bytes::new()
483        };
484        if cursor != header.len() {
485            return Err(trailing_header_error(header.len() - cursor));
486        }
487
488        let ext_fields = BinaryHeaderFields::new(payload)?;
489        Ok(RemotingCommand::from_binary_wire_parts(
490            code, language, version, opaque, flag, remark, ext_fields,
491        ))
492    }
493
494    /// Optimized map deserialization with capacity hint and better error handling
495    #[inline]
496    pub fn map_deserialize(
497        buffer: &mut BytesMut,
498        len: usize,
499    ) -> rocketmq_error::Result<HashMap<CheetahString, CheetahString>> {
500        if len == 0 {
501            return Ok(HashMap::new());
502        }
503        if len > buffer.remaining() {
504            return Err(decoding_error(len, buffer.remaining()));
505        }
506
507        // Request-header pairs are compact; this bounded estimate avoids the
508        // predictable rehashes caused by the previous 50-byte assumption.
509        let estimated_entries = Self::estimated_map_capacity(len);
510        let mut map = HashMap::with_capacity(estimated_entries);
511
512        let target_remaining = buffer.remaining().saturating_sub(len);
513
514        while buffer.remaining() > target_remaining {
515            // Read key (short length prefix)
516            let key = Self::read_str(buffer, true, len)?.ok_or_else(|| decoding_error(0, 0))?;
517
518            // Read value (long length prefix)
519            if let Some(value) = Self::read_str(buffer, false, len)? {
520                map.insert(key, value);
521            }
522        }
523
524        Ok(map)
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use bytes::BufMut;
531    use bytes::BytesMut;
532
533    use super::*;
534
535    fn minimal_header_without_ext_len() -> BytesMut {
536        let mut buf = BytesMut::new();
537        buf.put_i16(0);
538        buf.put_u8(LanguageCode::JAVA.get_code());
539        buf.put_i16(0);
540        buf.put_i32(0);
541        buf.put_i32(0);
542        buf.put_i32(0);
543        buf
544    }
545
546    #[test]
547    fn write_str_short_length() {
548        let mut buf = BytesMut::new();
549        let written = RocketMQSerializable::write_str(&mut buf, true, "test");
550        assert_eq!(written, 6);
551        assert_eq!(buf, BytesMut::from(&[0, 4, 116, 101, 115, 116][..]));
552    }
553
554    #[test]
555    fn write_str_long_length() {
556        let mut buf = BytesMut::new();
557        let written = RocketMQSerializable::write_str(&mut buf, false, "test");
558        assert_eq!(written, 8);
559        assert_eq!(buf, BytesMut::from(&[0, 0, 0, 4, 116, 101, 115, 116][..]));
560    }
561
562    #[test]
563    fn read_str_short_length() {
564        let mut buf = BytesMut::from(&[0, 4, 116, 101, 115, 116][..]);
565        let read = RocketMQSerializable::read_str(&mut buf, true, 10).unwrap();
566        assert_eq!(read, Some("test".into()));
567    }
568
569    #[test]
570    fn read_str_long_length() {
571        let mut buf = BytesMut::from(&[0, 0, 0, 4, 116, 101, 115, 116][..]);
572        let read = RocketMQSerializable::read_str(&mut buf, false, 10).unwrap();
573        assert_eq!(read, Some("test".into()));
574    }
575
576    #[test]
577    fn read_str_exceeds_limit() {
578        let mut buf = BytesMut::from(&[0, 0, 0, 4, 116, 101, 115, 116][..]);
579        let read = RocketMQSerializable::read_str(&mut buf, false, 2);
580        assert!(read.is_err());
581    }
582
583    #[test]
584    fn read_str_rejects_invalid_utf8() {
585        let mut buf = BytesMut::from(&[0, 2, 0xff, 0xfe][..]);
586        let read = RocketMQSerializable::read_str(&mut buf, true, 10);
587        assert!(read.is_err());
588    }
589
590    #[test]
591    fn map_serialize_empty() {
592        let map = HashMap::new();
593        let serialized = RocketMQSerializable::map_serialize(&map);
594        assert!(serialized.is_none());
595    }
596
597    #[test]
598    fn map_serialize_non_empty() {
599        let mut map = HashMap::new();
600        map.insert("key".into(), "value".into());
601        let serialized = RocketMQSerializable::map_serialize(&map).unwrap();
602        assert_eq!(
603            serialized,
604            BytesMut::from(&[0, 3, 107, 101, 121, 0, 0, 0, 5, 118, 97, 108, 117, 101][..])
605        );
606    }
607
608    #[test]
609    fn map_serialize_preserves_present_empty_value() {
610        let map = HashMap::from([("key".into(), CheetahString::new())]);
611
612        let serialized = RocketMQSerializable::map_serialize(&map).unwrap();
613
614        assert_eq!(serialized, BytesMut::from(&[0, 3, 107, 101, 121, 0, 0, 0, 0][..]));
615    }
616
617    #[test]
618    fn map_deserialize_empty() {
619        let mut buf = BytesMut::new();
620        let deserialized = RocketMQSerializable::map_deserialize(&mut buf, 0).unwrap();
621        assert!(deserialized.is_empty());
622    }
623
624    #[test]
625    fn map_deserialize_non_empty() {
626        let mut buf = BytesMut::from(&[0, 3, 107, 101, 121, 0, 0, 0, 5, 118, 97, 108, 117, 101][..]);
627        let deserialized = RocketMQSerializable::map_deserialize(&mut buf, 14).unwrap();
628        assert_eq!(deserialized, [("key".into(), "value".into())].iter().cloned().collect());
629    }
630
631    #[test]
632    fn map_capacity_estimate_covers_compact_request_headers_and_stays_bounded() {
633        assert_eq!(RocketMQSerializable::estimated_map_capacity(1), 4);
634        assert_eq!(RocketMQSerializable::estimated_map_capacity(292), 18);
635        assert_eq!(RocketMQSerializable::estimated_map_capacity(385), 24);
636        assert_eq!(RocketMQSerializable::estimated_map_capacity(usize::MAX), 1024);
637    }
638
639    #[test]
640    fn map_deserialize_normalizes_zero_length_value_to_absent() {
641        let mut buf = BytesMut::from(&[0, 3, 107, 101, 121, 0, 0, 0, 0][..]);
642
643        let deserialized = RocketMQSerializable::map_deserialize(&mut buf, 9).unwrap();
644
645        assert!(deserialized.is_empty());
646        assert!(buf.is_empty());
647    }
648
649    #[test]
650    fn extension_field_length_checks_limit_and_limit_plus_one() {
651        assert_eq!(
652            RocketMQSerializable::checked_ext_fields_length(i32::MAX as usize).unwrap(),
653            i32::MAX
654        );
655        assert!(matches!(
656            RocketMQSerializable::checked_ext_fields_length(i32::MAX as usize + 1),
657            Err(ProtocolContractViolation::ExtensionFieldsLengthOverflow)
658        ));
659        assert_eq!(
660            RocketMQSerializable::checked_dynamic_value_length(i32::MAX as usize).unwrap(),
661            i32::MAX
662        );
663        assert!(matches!(
664            RocketMQSerializable::checked_dynamic_value_length(i32::MAX as usize + 1),
665            Err(ProtocolContractViolation::DynamicValueLengthOverflow)
666        ));
667    }
668
669    #[test]
670    fn dynamic_key_length_java_compatible() {
671        let maximum = i16::MAX as usize;
672
673        assert_eq!(
674            RocketMQSerializable::checked_dynamic_key_length(maximum - 1).unwrap(),
675            (i16::MAX - 1) as u16
676        );
677        assert_eq!(
678            RocketMQSerializable::checked_dynamic_key_length(maximum).unwrap(),
679            i16::MAX as u16
680        );
681        assert!(matches!(
682            RocketMQSerializable::checked_dynamic_key_length(maximum + 1),
683            Err(ProtocolContractViolation::DynamicKeyLengthOverflow)
684        ));
685    }
686
687    #[test]
688    fn rocketmq_protocol_decode_rejects_short_fixed_header_without_panic() {
689        let mut buf = BytesMut::from(&[0_u8; 12][..]);
690        if RocketMQSerializable::rocket_mq_protocol_decode(&mut buf, 12).is_ok() {
691            panic!("short fixed header should decode to error");
692        }
693    }
694
695    #[test]
696    fn rocketmq_protocol_decode_rejects_missing_ext_length_without_panic() {
697        let mut buf = minimal_header_without_ext_len();
698        let header_len = buf.len();
699        if RocketMQSerializable::rocket_mq_protocol_decode(&mut buf, header_len).is_ok() {
700            panic!("missing ext length should decode to error");
701        }
702    }
703
704    #[test]
705    fn rocketmq_protocol_decode_rejects_truncated_ext_fields_without_panic() {
706        let mut buf = minimal_header_without_ext_len();
707        buf.put_i32(10);
708        let header_len = buf.len();
709        if RocketMQSerializable::rocket_mq_protocol_decode(&mut buf, header_len).is_ok() {
710            panic!("truncated ext fields should decode to error");
711        }
712    }
713
714    #[test]
715    fn rocketmq_protocol_decode_rejects_bytes_after_declared_ext_fields() {
716        let mut buf = minimal_header_without_ext_len();
717        buf.put_i32(0);
718        buf.put_u8(1);
719        let header_len = buf.len();
720
721        assert!(RocketMQSerializable::rocket_mq_protocol_decode(&mut buf, header_len).is_err());
722    }
723
724    #[test]
725    fn bytes_decoder_preserves_remark_and_extension_fields() {
726        let mut buf = BytesMut::new();
727        buf.put_i16(10);
728        buf.put_u8(LanguageCode::RUST.get_code());
729        buf.put_i16(501);
730        buf.put_i32(7);
731        buf.put_i32(1);
732        RocketMQSerializable::write_str(&mut buf, false, "remark");
733        let ext_length_offset = buf.len();
734        buf.put_i32(0);
735        let ext_start = buf.len();
736        buf.put_u16(3);
737        buf.extend_from_slice(b"key");
738        buf.put_i32(5);
739        buf.extend_from_slice(b"value");
740        let ext_length = (buf.len() - ext_start) as i32;
741        buf[ext_length_offset..ext_length_offset + 4].copy_from_slice(&ext_length.to_be_bytes());
742
743        let command = RocketMQSerializable::rocket_mq_protocol_decode_bytes(buf.freeze()).unwrap();
744
745        assert_eq!(command.code(), 10);
746        assert_eq!(command.language(), LanguageCode::RUST);
747        assert_eq!(command.version(), 501);
748        assert_eq!(command.opaque(), 7);
749        assert_eq!(command.flag(), 1);
750        assert_eq!(command.remark().map(CheetahString::as_str), Some("remark"));
751        assert_eq!(command.ext_fields().unwrap().get("key").unwrap(), "value");
752    }
753
754    #[test]
755    fn binary_decode_does_not_generate_an_outbound_request_id() {
756        let mut buf = BytesMut::new();
757        buf.put_i16(105);
758        buf.put_u8(LanguageCode::JAVA.get_code());
759        buf.put_i16(321);
760        buf.put_i32(0x0102_0304);
761        buf.put_i32(3);
762        RocketMQSerializable::write_str(&mut buf, false, "wire");
763        buf.put_i32(0);
764        let before = crate::protocol::remoting_command::request_id_generation_count();
765
766        let command = RocketMQSerializable::rocket_mq_protocol_decode_bytes(buf.freeze()).unwrap();
767
768        assert_eq!(
769            crate::protocol::remoting_command::request_id_generation_count(),
770            before,
771            "binary decode must not consume an outbound correlation ID"
772        );
773        assert_eq!(command.code(), 105);
774        assert_eq!(command.language(), LanguageCode::JAVA);
775        assert_eq!(command.version(), 321);
776        assert_eq!(command.opaque(), 0x0102_0304);
777        assert_eq!(command.flag(), 3);
778        assert_eq!(command.remark().map(CheetahString::as_str), Some("wire"));
779        assert!(command.ext_fields().is_some_and(HashMap::is_empty));
780        assert_eq!(command.get_serialize_type(), crate::protocol::SerializeType::ROCKETMQ);
781    }
782}