Skip to main content

rocketmq_protocol/protocol/
encoded_frame.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 bytes::BufMut;
16use bytes::Bytes;
17use bytes::BytesMut;
18use rocketmq_error::Result;
19
20use super::remoting_command::RemotingCommand;
21
22const FRAME_PREFIX_BYTES: usize = 8;
23const SERIALIZE_TYPE_BYTES: usize = 4;
24const MAX_HEADER_BYTES: usize = 0x00ff_ffff;
25
26/// Immutable RocketMQ wire frame split into prefix, serialized header, and body segments.
27///
28/// Keeping the body in its existing [`Bytes`] allocation lets plaintext transports use vectored
29/// writes without first copying the complete frame into a second contiguous buffer. The frame's
30/// exact [`Self::encoded_len`] is also available before transport staging or TLS aggregation.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct EncodedFrame {
33    prefix: [u8; FRAME_PREFIX_BYTES],
34    header: Bytes,
35    body: Bytes,
36}
37
38/// Immutable RocketMQ prefix and serialized header for a body stored outside memory.
39///
40/// The external body must contain exactly [`Self::body_len`] bytes and must be appended directly
41/// after [`Self::segments`]. This type lets transports send a leased file region without first
42/// materializing the body as [`Bytes`].
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct EncodedFrameHead {
45    prefix: [u8; FRAME_PREFIX_BYTES],
46    header: Bytes,
47    body_len: usize,
48}
49
50impl EncodedFrame {
51    /// Encodes a command into immutable wire segments.
52    ///
53    /// # Errors
54    ///
55    /// Returns a serialization error when the command header cannot be encoded, the header exceeds
56    /// RocketMQ's 24-bit header-length field, or the complete frame exceeds its signed 32-bit
57    /// length field.
58    pub fn from_command(mut command: RemotingCommand) -> Result<Self> {
59        let body = command.take_body().unwrap_or_default();
60        let (prefix, header) = encode_header_segments(&mut command, body.len())?;
61        validate_announced_payload_len(&prefix, header.len(), body.len())?;
62        Ok(Self { prefix, header, body })
63    }
64
65    /// Returns the complete encoded wire size, including the four-byte total-length prefix.
66    #[inline]
67    #[must_use]
68    pub fn encoded_len(&self) -> usize {
69        FRAME_PREFIX_BYTES + self.header.len() + self.body.len()
70    }
71
72    /// Returns the immutable prefix, header, and body slices in wire order.
73    #[inline]
74    #[must_use]
75    pub fn segments(&self) -> [&[u8]; 3] {
76        [&self.prefix, self.header.as_ref(), self.body.as_ref()]
77    }
78
79    /// Appends the wire frame to a caller-owned contiguous buffer.
80    ///
81    /// This is intended for TLS record input and compatibility codecs. Plaintext transports should
82    /// preserve [`Self::segments`] through to vectored socket writes.
83    pub fn copy_to(&self, destination: &mut BytesMut) {
84        destination.reserve(self.encoded_len());
85        destination.put_slice(&self.prefix);
86        destination.put_slice(&self.header);
87        destination.put_slice(&self.body);
88    }
89
90    /// Materializes the complete frame as contiguous immutable bytes.
91    ///
92    /// Prefer [`Self::segments`] when the destination accepts vectored writes.
93    #[must_use]
94    pub fn into_bytes(self) -> Bytes {
95        let mut contiguous = BytesMut::with_capacity(self.encoded_len());
96        self.copy_to(&mut contiguous);
97        contiguous.freeze()
98    }
99}
100
101impl EncodedFrameHead {
102    /// Encodes a command prefix/header for an external body of `body_len` bytes.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error when the command already has an in-memory body, header serialization
107    /// fails, or the complete frame exceeds RocketMQ's signed 32-bit wire-length limit.
108    pub fn from_command_and_body_len(mut command: RemotingCommand, body_len: usize) -> Result<Self> {
109        if command.body().is_some() {
110            return Err(crate::error::serialization_encode_failed(
111                "remoting-command-file-body",
112                "command must not contain an in-memory body when an external body length is supplied",
113            ));
114        }
115        let (prefix, header) = encode_header_segments(&mut command, body_len)?;
116        validate_announced_payload_len(&prefix, header.len(), body_len)?;
117        Ok(Self {
118            prefix,
119            header,
120            body_len,
121        })
122    }
123
124    /// Returns the immutable prefix and serialized header slices in wire order.
125    #[inline]
126    #[must_use]
127    pub fn segments(&self) -> [&[u8]; 2] {
128        [&self.prefix, self.header.as_ref()]
129    }
130
131    /// Returns the exact number of external body bytes required by this frame.
132    #[inline]
133    #[must_use]
134    pub const fn body_len(&self) -> usize {
135        self.body_len
136    }
137
138    /// Returns the complete encoded frame size, including the external body.
139    #[inline]
140    #[must_use]
141    pub fn encoded_len(&self) -> usize {
142        FRAME_PREFIX_BYTES + self.header.len() + self.body_len
143    }
144}
145
146fn encode_header_segments(command: &mut RemotingCommand, body_len: usize) -> Result<([u8; FRAME_PREFIX_BYTES], Bytes)> {
147    let mut encoded_header = BytesMut::new();
148    command.try_fast_header_encode_with_body_length(&mut encoded_header, body_len)?;
149    if encoded_header.len() < FRAME_PREFIX_BYTES {
150        return Err(crate::error::serialization_encode_failed(
151            "remoting-command",
152            "encoded header omitted the RocketMQ frame prefix",
153        ));
154    }
155    let prefix_bytes = encoded_header.split_to(FRAME_PREFIX_BYTES);
156    let header = encoded_header.freeze();
157    if header.len() > MAX_HEADER_BYTES {
158        return Err(crate::error::serialization_encode_failed(
159            "remoting-command",
160            format!(
161                "encoded header is {} bytes, exceeding the 24-bit wire limit",
162                header.len()
163            ),
164        ));
165    }
166    let mut prefix = [0_u8; FRAME_PREFIX_BYTES];
167    prefix.copy_from_slice(&prefix_bytes);
168    let announced_header = u32::from_be_bytes([prefix[4], prefix[5], prefix[6], prefix[7]]) & MAX_HEADER_BYTES as u32;
169    if announced_header as usize != header.len() {
170        return Err(crate::error::serialization_encode_failed(
171            "remoting-command",
172            "fast header encoder produced an inconsistent header length",
173        ));
174    }
175    Ok((prefix, header))
176}
177
178fn checked_payload_len(header_len: usize, body_len: usize) -> Result<i32> {
179    let payload_len = SERIALIZE_TYPE_BYTES
180        .checked_add(header_len)
181        .and_then(|length| length.checked_add(body_len))
182        .ok_or_else(|| {
183            crate::error::serialization_encode_failed("remoting-command", "encoded frame length overflow")
184        })?;
185    i32::try_from(payload_len).map_err(|_| {
186        crate::error::serialization_encode_failed(
187            "remoting-command",
188            format!("encoded payload is {payload_len} bytes, exceeding the signed 32-bit wire limit"),
189        )
190    })
191}
192
193fn validate_announced_payload_len(prefix: &[u8; FRAME_PREFIX_BYTES], header_len: usize, body_len: usize) -> Result<()> {
194    let expected = checked_payload_len(header_len, body_len)?;
195    let announced = i32::from_be_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]);
196    if announced != expected {
197        return Err(crate::error::serialization_encode_failed(
198            "remoting-command",
199            "fast header encoder produced inconsistent wire lengths",
200        ));
201    }
202    Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207    use std::collections::HashMap;
208
209    use bytes::BufMut;
210    use bytes::Bytes;
211    use bytes::BytesMut;
212    use cheetah_string::CheetahString;
213
214    use super::EncodedFrame;
215    use super::EncodedFrameHead;
216    use crate::protocol::command_custom_header::CommandCustomHeader;
217    use crate::protocol::command_custom_header::HeaderEncodeCapability;
218    use crate::protocol::header::client_request_header::GetRouteInfoRequestHeader;
219    use crate::protocol::header::message_operation_header::send_message_request_header_v2::SendMessageRequestHeaderV2;
220    use crate::protocol::remoting_command::RemotingCommand;
221    use crate::protocol::SerializeType;
222    use crate::ProtocolContractViolation;
223
224    struct FailingDirectHeader;
225
226    impl CommandCustomHeader for FailingDirectHeader {
227        fn to_map(&self) -> Option<HashMap<CheetahString, CheetahString>> {
228            Some(HashMap::new())
229        }
230
231        fn encode_capability(&self) -> HeaderEncodeCapability {
232            HeaderEncodeCapability::DirectBinary
233        }
234
235        fn encode_direct_binary(&self, _out: &mut BytesMut) -> Result<(), ProtocolContractViolation> {
236            Err(ProtocolContractViolation::FastCodecUnavailable {
237                header: "FailingDirectHeader",
238            })
239        }
240    }
241
242    fn legacy_contiguous(mut command: RemotingCommand) -> Bytes {
243        let mut bytes = BytesMut::new();
244        command.fast_header_encode(&mut bytes);
245        if let Some(body) = command.take_body() {
246            bytes.put(body);
247        }
248        bytes.freeze()
249    }
250
251    #[test]
252    fn encoded_frame_matches_json_contiguous_encoding_byte_for_byte() {
253        let command = RemotingCommand::create_remoting_command(105)
254            .set_opaque(73)
255            .set_remark("segmented-json")
256            .set_command_custom_header(GetRouteInfoRequestHeader::new("json-topic", None))
257            .set_body(Bytes::from_static(b"json-body"));
258        let expected = legacy_contiguous(command.clone());
259        let frame = EncodedFrame::from_command(command).expect("JSON command should encode");
260
261        assert_eq!(frame.encoded_len(), expected.len());
262        assert_eq!(frame.into_bytes(), expected);
263    }
264
265    #[test]
266    fn encoded_frame_matches_rocketmq_contiguous_encoding_byte_for_byte() {
267        let command = RemotingCommand::create_remoting_command(106)
268            .set_opaque(74)
269            .set_serialize_type(SerializeType::ROCKETMQ)
270            .set_command_custom_header(GetRouteInfoRequestHeader::new("binary-topic", Some(false)))
271            .set_body(Bytes::from_static(b"binary-body"));
272        let expected = legacy_contiguous(command.clone());
273        let frame = EncodedFrame::from_command(command).expect("RocketMQ command should encode");
274
275        assert_eq!(frame.encoded_len(), expected.len());
276        assert_eq!(frame.into_bytes(), expected);
277    }
278
279    #[test]
280    fn encoded_frame_exposes_prefix_header_and_body_without_coalescing() {
281        let body = Bytes::from_static(b"owned-body");
282        let frame = EncodedFrame::from_command(
283            RemotingCommand::create_remoting_command(107)
284                .set_opaque(75)
285                .set_body(body.clone()),
286        )
287        .expect("command should encode");
288        let [prefix, header, encoded_body] = frame.segments();
289
290        assert_eq!(prefix.len(), 8);
291        assert!(!header.is_empty());
292        assert_eq!(encoded_body, body.as_ref());
293        assert_eq!(frame.encoded_len(), prefix.len() + header.len() + encoded_body.len());
294    }
295
296    #[test]
297    fn encoded_frame_head_matches_the_complete_frame_byte_for_byte() {
298        let body = Bytes::from(vec![0x5a; 64 * 1024]);
299        let command = RemotingCommand::create_remoting_command(108)
300            .set_opaque(91)
301            .set_remark("external-file-body");
302        let head = EncodedFrameHead::from_command_and_body_len(command.clone(), body.len()).unwrap();
303        let complete = EncodedFrame::from_command(command.set_body(body.clone()))
304            .unwrap()
305            .into_bytes();
306        let [prefix, header] = head.segments();
307        let mut reconstructed = BytesMut::with_capacity(head.encoded_len());
308        reconstructed.put_slice(prefix);
309        reconstructed.put_slice(header);
310        reconstructed.put_slice(&body);
311
312        assert_eq!(reconstructed.freeze(), complete);
313        assert_eq!(head.body_len(), body.len());
314    }
315
316    #[test]
317    fn frame_head_and_complete_frame_match_for_all_body_sizes_and_protocols() {
318        for serialize_type in [SerializeType::JSON, SerializeType::ROCKETMQ] {
319            for body_len in [0, 1, 128, 4 * 1024, 64 * 1024, 1024 * 1024, 4 * 1024 * 1024] {
320                let body = Bytes::from(vec![0x5a; body_len]);
321                let command = RemotingCommand::create_remoting_command(108)
322                    .set_opaque(92)
323                    .set_serialize_type(serialize_type)
324                    .set_remark("body-length-matrix");
325                let head = EncodedFrameHead::from_command_and_body_len(command.clone(), body_len).unwrap();
326                let complete = EncodedFrame::from_command(command.set_body(body.clone()))
327                    .unwrap()
328                    .into_bytes();
329                let [prefix, header] = head.segments();
330                let mut reconstructed = BytesMut::with_capacity(head.encoded_len());
331                reconstructed.put_slice(prefix);
332                reconstructed.put_slice(header);
333                reconstructed.put_slice(&body);
334
335                assert_eq!(
336                    reconstructed.freeze(),
337                    complete,
338                    "{serialize_type:?} body_len={body_len}"
339                );
340            }
341        }
342    }
343
344    #[test]
345    fn explicit_body_length_rejects_an_in_memory_body_mismatch_atomically() {
346        let mut command = RemotingCommand::create_remoting_command(109).set_body(Bytes::from_static(b"body"));
347        let mut destination = BytesMut::from(&b"existing"[..]);
348
349        assert!(command
350            .try_fast_header_encode_with_body_length(&mut destination, 3)
351            .is_err());
352        assert_eq!(destination.as_ref(), b"existing");
353    }
354
355    #[test]
356    fn encoded_frame_head_rejects_ambiguous_or_oversized_bodies() {
357        let with_body = RemotingCommand::create_remoting_command(109).set_body(Bytes::from_static(b"ambiguous"));
358        assert!(EncodedFrameHead::from_command_and_body_len(with_body, 9).is_err());
359
360        let command = RemotingCommand::create_remoting_command(110);
361        assert!(EncodedFrameHead::from_command_and_body_len(command, i32::MAX as usize).is_err());
362    }
363
364    #[test]
365    fn encoded_frame_canonicalizes_ext_field_order_for_all_protocols() {
366        for serialize_type in [SerializeType::JSON, SerializeType::ROCKETMQ] {
367            let first = HashMap::from([
368                (
369                    CheetahString::from_static_str("zeta"),
370                    CheetahString::from_static_str("last"),
371                ),
372                (
373                    CheetahString::from_static_str("alpha"),
374                    CheetahString::from_static_str("first"),
375                ),
376            ]);
377            let second = HashMap::from([
378                (
379                    CheetahString::from_static_str("alpha"),
380                    CheetahString::from_static_str("first"),
381                ),
382                (
383                    CheetahString::from_static_str("zeta"),
384                    CheetahString::from_static_str("last"),
385                ),
386            ]);
387            let encode = |ext_fields| {
388                EncodedFrame::from_command(
389                    RemotingCommand::create_remoting_command(108)
390                        .set_opaque(76)
391                        .set_serialize_type(serialize_type)
392                        .set_ext_fields(ext_fields),
393                )
394                .expect("command should encode")
395                .into_bytes()
396            };
397
398            assert_eq!(encode(first), encode(second));
399        }
400    }
401
402    #[test]
403    fn direct_header_encoding_is_identical_after_multiple_command_clones() {
404        let header = SendMessageRequestHeaderV2 {
405            a: "producer-a".into(),
406            b: "topic-a".into(),
407            c: "TBW102".into(),
408            d: 4,
409            e: 2,
410            f: 0,
411            g: 42,
412            h: 1,
413            ..Default::default()
414        };
415        let original = RemotingCommand::create_remoting_command(310)
416            .set_serialize_type(SerializeType::ROCKETMQ)
417            .set_command_custom_header(header);
418        let first_clone = original.clone();
419        let second_clone = first_clone.clone();
420
421        let frames = [original, first_clone, second_clone].map(|command| {
422            EncodedFrame::from_command(command)
423                .expect("shared direct header should encode")
424                .into_bytes()
425        });
426        assert_eq!(frames[0], frames[1]);
427        assert_eq!(frames[1], frames[2]);
428
429        for frame in frames {
430            let mut input = BytesMut::from(frame.as_ref());
431            let decoded = RemotingCommand::decode(&mut input)
432                .expect("frame should decode")
433                .expect("frame should be complete");
434            let decoded_header = decoded
435                .decode_command_custom_header::<SendMessageRequestHeaderV2>()
436                .expect("direct fields should remain present");
437            assert_eq!(decoded_header.a.as_str(), "producer-a");
438            assert_eq!(decoded_header.b.as_str(), "topic-a");
439            assert_eq!(decoded_header.g, 42);
440        }
441    }
442
443    #[test]
444    fn fallible_direct_header_encoding_rolls_back_the_destination() {
445        let mut command = RemotingCommand::create_remoting_command(311)
446            .set_serialize_type(SerializeType::ROCKETMQ)
447            .set_command_custom_header(FailingDirectHeader);
448        let mut destination = BytesMut::from(&b"existing"[..]);
449
450        assert!(command.try_fast_header_encode(&mut destination).is_err());
451        assert_eq!(destination.as_ref(), b"existing");
452    }
453
454    #[test]
455    fn materialized_direct_header_is_not_encoded_twice_after_clone() {
456        let header = SendMessageRequestHeaderV2 {
457            a: "producer-b".into(),
458            b: "topic-b".into(),
459            c: "TBW102".into(),
460            d: 4,
461            e: 3,
462            f: 0,
463            g: 43,
464            h: 1,
465            ..Default::default()
466        };
467        let mut original = RemotingCommand::create_remoting_command(312)
468            .set_serialize_type(SerializeType::ROCKETMQ)
469            .set_command_custom_header(header);
470        original.materialize_custom_header_to_ext_fields();
471        let cloned = original.clone();
472
473        let original_frame = EncodedFrame::from_command(original).unwrap().into_bytes();
474        let cloned_frame = EncodedFrame::from_command(cloned).unwrap().into_bytes();
475        assert_eq!(original_frame, cloned_frame);
476    }
477}