Skip to main content

rocketmq_protocol/protocol/header_codec/
value.rs

1// Copyright 2026 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::BytesMut;
16use cheetah_string::CheetahString;
17use rocketmq_model::boundary_type::BoundaryType;
18
19use super::private::Sealed;
20use super::write_json_string;
21use super::ProtocolContractViolation;
22
23/// Protocol-reviewed value categories supported by typed request headers.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum HeaderValueKind {
26    /// UTF-8 text.
27    String,
28    /// Canonical ASCII `true` or `false`.
29    Bool,
30    /// Signed 32-bit decimal integer.
31    I32,
32    /// Signed 64-bit decimal integer.
33    I64,
34    /// Unsigned 32-bit decimal integer.
35    U32,
36    /// Unsigned 64-bit decimal integer.
37    U64,
38    /// RocketMQ offset boundary selector.
39    BoundaryType,
40}
41
42/// A signed Java numeric range applied to an unsigned Rust field.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum HeaderRange {
45    /// Java `int`/`Integer`: `0..=i32::MAX` for an unsigned Rust field.
46    I32,
47    /// Java `long`/`Long`: `0..=i64::MAX` for an unsigned Rust field.
48    I64,
49}
50
51/// Static field metadata used for typed conversion and diagnostics.
52///
53/// Derive output creates one constant context per field, so successful codec
54/// paths never allocate diagnostic metadata.
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub struct HeaderFieldContext {
57    /// Stable header type identifier.
58    pub header: &'static str,
59    /// Canonical wire key.
60    pub key: &'static str,
61    /// Expected value category.
62    pub kind: HeaderValueKind,
63    /// Optional Java signed-range constraint.
64    pub range: Option<HeaderRange>,
65}
66
67impl HeaderFieldContext {
68    /// Creates static metadata for one header field.
69    pub const fn new(
70        header: &'static str,
71        key: &'static str,
72        kind: HeaderValueKind,
73        range: Option<HeaderRange>,
74    ) -> Self {
75        Self {
76            header,
77            key,
78            kind,
79            range,
80        }
81    }
82}
83
84/// A protocol-approved request-header wire value.
85///
86/// Implementations must keep `encoded_len` and `write_ascii` byte-for-byte
87/// consistent. This trait is sealed: new wire types require an explicit
88/// implementation and compatibility review in `rocketmq-protocol`.
89pub trait HeaderValue: Sealed + Sized {
90    /// Static value category used by generated schemas and diagnostics.
91    const KIND: HeaderValueKind;
92
93    /// Whether every value of this type fits Java's signed 32-bit wire length.
94    const ALWAYS_FITS_WIRE_LENGTH: bool = false;
95
96    /// Produces the owned value required by a [`crate::HeaderMap`].
97    fn to_map_value(&self) -> CheetahString;
98
99    /// Returns the exact number of bytes written by [`Self::write_ascii`].
100    fn encoded_len(&self) -> usize;
101
102    /// Appends the canonical UTF-8/ASCII wire representation without a
103    /// temporary `String` for scalar values.
104    fn write_ascii(&self, out: &mut BytesMut);
105
106    /// Appends the canonical extension-field value as a JSON string.
107    ///
108    /// Scalar implementations use their allocation-free ASCII form. Text
109    /// implementations override this method to apply JSON escaping.
110    #[inline]
111    fn write_json_string(&self, out: &mut BytesMut) {
112        out.extend_from_slice(b"\"");
113        self.write_ascii(out);
114        out.extend_from_slice(b"\"");
115    }
116
117    /// Decodes a complete map value according to the field context.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`ProtocolContractViolation::InvalidValue`] for malformed or overflowing
122    /// scalar values, or [`ProtocolContractViolation::JavaRange`] when an unsigned value
123    /// exceeds its field's declared signed Java range. Errors never retain
124    /// `raw`.
125    fn decode(raw: &str, context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation>;
126}
127
128/// Validates an unsigned value against its field-level signed Java range.
129///
130/// A context without a range is a Rust extension and remains unrestricted.
131/// Derive output calls this helper once before sink dispatch; unsigned decoders
132/// call it once after successful parsing.
133///
134/// # Errors
135///
136/// Returns [`ProtocolContractViolation::JavaRange`] when `value` exceeds the declared
137/// range. The error contains only static field metadata.
138#[inline]
139pub fn validate_unsigned_java_range(value: u64, context: HeaderFieldContext) -> Result<(), ProtocolContractViolation> {
140    let in_range = match context.range {
141        None => true,
142        Some(HeaderRange::I32) => value <= i32::MAX as u64,
143        Some(HeaderRange::I64) => value <= i64::MAX as u64,
144    };
145    if in_range {
146        Ok(())
147    } else {
148        Err(ProtocolContractViolation::JavaRange {
149            header: context.header,
150            key: context.key,
151        })
152    }
153}
154
155#[inline]
156const fn invalid_value(context: HeaderFieldContext) -> ProtocolContractViolation {
157    ProtocolContractViolation::InvalidValue {
158        header: context.header,
159        key: context.key,
160        expected: context.kind,
161    }
162}
163
164#[inline]
165fn signed_decimal_len(value: i64) -> usize {
166    unsigned_decimal_len(value.unsigned_abs()) + usize::from(value < 0)
167}
168
169#[inline]
170fn unsigned_decimal_len(value: u64) -> usize {
171    if value == 0 {
172        1
173    } else {
174        value.ilog10() as usize + 1
175    }
176}
177
178impl Sealed for CheetahString {}
179
180impl HeaderValue for CheetahString {
181    const KIND: HeaderValueKind = HeaderValueKind::String;
182    const ALWAYS_FITS_WIRE_LENGTH: bool = false;
183
184    #[inline]
185    fn to_map_value(&self) -> CheetahString {
186        self.clone()
187    }
188
189    #[inline]
190    fn encoded_len(&self) -> usize {
191        self.len()
192    }
193
194    #[inline]
195    fn write_ascii(&self, out: &mut BytesMut) {
196        out.extend_from_slice(self.as_bytes());
197    }
198
199    #[inline]
200    fn write_json_string(&self, out: &mut BytesMut) {
201        write_json_string(out, self.as_str());
202    }
203
204    #[inline]
205    fn decode(raw: &str, _context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation> {
206        Ok(CheetahString::from_slice(raw))
207    }
208}
209
210impl Sealed for String {}
211
212impl HeaderValue for String {
213    const KIND: HeaderValueKind = HeaderValueKind::String;
214    const ALWAYS_FITS_WIRE_LENGTH: bool = false;
215
216    #[inline]
217    fn to_map_value(&self) -> CheetahString {
218        CheetahString::from_slice(self)
219    }
220
221    #[inline]
222    fn encoded_len(&self) -> usize {
223        self.len()
224    }
225
226    #[inline]
227    fn write_ascii(&self, out: &mut BytesMut) {
228        out.extend_from_slice(self.as_bytes());
229    }
230
231    #[inline]
232    fn write_json_string(&self, out: &mut BytesMut) {
233        write_json_string(out, self);
234    }
235
236    #[inline]
237    fn decode(raw: &str, _context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation> {
238        Ok(raw.to_owned())
239    }
240}
241
242impl Sealed for bool {}
243
244impl HeaderValue for bool {
245    const KIND: HeaderValueKind = HeaderValueKind::Bool;
246    const ALWAYS_FITS_WIRE_LENGTH: bool = true;
247
248    #[inline]
249    fn to_map_value(&self) -> CheetahString {
250        CheetahString::from_static_str(if *self { "true" } else { "false" })
251    }
252
253    #[inline]
254    fn encoded_len(&self) -> usize {
255        if *self {
256            4
257        } else {
258            5
259        }
260    }
261
262    #[inline]
263    fn write_ascii(&self, out: &mut BytesMut) {
264        out.extend_from_slice(if *self { b"true" } else { b"false" });
265    }
266
267    #[inline]
268    fn decode(raw: &str, context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation> {
269        if raw.eq_ignore_ascii_case("true") {
270            Ok(true)
271        } else if raw.eq_ignore_ascii_case("false") {
272            Ok(false)
273        } else {
274            Err(invalid_value(context))
275        }
276    }
277}
278
279macro_rules! impl_signed_header_value {
280    ($ty:ty, $kind:ident) => {
281        impl Sealed for $ty {}
282
283        impl HeaderValue for $ty {
284            const KIND: HeaderValueKind = HeaderValueKind::$kind;
285            const ALWAYS_FITS_WIRE_LENGTH: bool = true;
286
287            #[inline]
288            fn to_map_value(&self) -> CheetahString {
289                let mut buffer = itoa::Buffer::new();
290                CheetahString::from_slice(buffer.format(*self))
291            }
292
293            #[inline]
294            fn encoded_len(&self) -> usize {
295                signed_decimal_len(*self as i64)
296            }
297
298            #[inline]
299            fn write_ascii(&self, out: &mut BytesMut) {
300                let mut buffer = itoa::Buffer::new();
301                out.extend_from_slice(buffer.format(*self).as_bytes());
302            }
303
304            #[inline]
305            fn decode(raw: &str, context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation> {
306                raw.parse::<$ty>().map_err(|_| invalid_value(context))
307            }
308        }
309    };
310}
311
312macro_rules! impl_unsigned_header_value {
313    ($ty:ty, $kind:ident) => {
314        impl Sealed for $ty {}
315
316        impl HeaderValue for $ty {
317            const KIND: HeaderValueKind = HeaderValueKind::$kind;
318            const ALWAYS_FITS_WIRE_LENGTH: bool = true;
319
320            #[inline]
321            fn to_map_value(&self) -> CheetahString {
322                let mut buffer = itoa::Buffer::new();
323                CheetahString::from_slice(buffer.format(*self))
324            }
325
326            #[inline]
327            fn encoded_len(&self) -> usize {
328                unsigned_decimal_len(*self as u64)
329            }
330
331            #[inline]
332            fn write_ascii(&self, out: &mut BytesMut) {
333                let mut buffer = itoa::Buffer::new();
334                out.extend_from_slice(buffer.format(*self).as_bytes());
335            }
336
337            #[inline]
338            fn decode(raw: &str, context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation> {
339                let value = raw.parse::<$ty>().map_err(|_| invalid_value(context))?;
340                validate_unsigned_java_range(value as u64, context)?;
341                Ok(value)
342            }
343        }
344    };
345}
346
347impl_signed_header_value!(i32, I32);
348impl_signed_header_value!(i64, I64);
349impl_unsigned_header_value!(u32, U32);
350impl_unsigned_header_value!(u64, U64);
351
352impl Sealed for BoundaryType {}
353
354impl HeaderValue for BoundaryType {
355    const KIND: HeaderValueKind = HeaderValueKind::BoundaryType;
356    const ALWAYS_FITS_WIRE_LENGTH: bool = true;
357
358    #[inline]
359    fn to_map_value(&self) -> CheetahString {
360        CheetahString::from_static_str(match self {
361            BoundaryType::Lower => "LOWER",
362            BoundaryType::Upper => "UPPER",
363        })
364    }
365
366    #[inline]
367    fn encoded_len(&self) -> usize {
368        match self {
369            BoundaryType::Lower => 5,
370            BoundaryType::Upper => 5,
371        }
372    }
373
374    #[inline]
375    fn write_ascii(&self, out: &mut BytesMut) {
376        out.extend_from_slice(match self {
377            BoundaryType::Lower => b"LOWER",
378            BoundaryType::Upper => b"UPPER",
379        });
380    }
381
382    #[inline]
383    fn decode(raw: &str, _context: HeaderFieldContext) -> Result<Self, ProtocolContractViolation> {
384        Ok(BoundaryType::get_type(raw))
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    const fn context(kind: HeaderValueKind, range: Option<HeaderRange>) -> HeaderFieldContext {
393        HeaderFieldContext::new("ExampleHeader", "value", kind, range)
394    }
395
396    fn assert_encoding<T>(value: T, expected: &str)
397    where
398        T: HeaderValue,
399    {
400        let mut out = BytesMut::new();
401        value.write_ascii(&mut out);
402        assert_eq!(value.encoded_len(), expected.len());
403        assert_eq!(out.as_ref(), expected.as_bytes());
404        assert_eq!(value.to_map_value().as_str(), expected);
405    }
406
407    #[test]
408    fn strings_preserve_empty_unicode_and_exact_lengths() {
409        for raw in ["", "topic", "主题-🚀"] {
410            let cheetah = CheetahString::decode(raw, context(HeaderValueKind::String, None)).unwrap();
411            assert_encoding(cheetah, raw);
412
413            let owned = String::decode(raw, context(HeaderValueKind::String, None)).unwrap();
414            assert_encoding(owned, raw);
415        }
416    }
417
418    #[test]
419    fn signed_integers_cover_extrema_zero_and_overflow() {
420        for value in [i32::MIN, -1, 0, 1, i32::MAX] {
421            let text = value.to_string();
422            assert_eq!(i32::decode(&text, context(HeaderValueKind::I32, None)).unwrap(), value);
423            assert_encoding(value, &text);
424        }
425        for value in [i64::MIN, -1, 0, 1, i64::MAX] {
426            let text = value.to_string();
427            assert_eq!(i64::decode(&text, context(HeaderValueKind::I64, None)).unwrap(), value);
428            assert_encoding(value, &text);
429        }
430
431        assert_eq!(i32::decode("+1", context(HeaderValueKind::I32, None)).unwrap(), 1);
432        for raw in ["2147483648", "-2147483649", " 1", "1 "] {
433            assert!(matches!(
434                i32::decode(raw, context(HeaderValueKind::I32, None)),
435                Err(ProtocolContractViolation::InvalidValue { .. })
436            ));
437        }
438        for raw in ["9223372036854775808", "-9223372036854775809"] {
439            assert!(matches!(
440                i64::decode(raw, context(HeaderValueKind::I64, None)),
441                Err(ProtocolContractViolation::InvalidValue { .. })
442            ));
443        }
444    }
445
446    #[test]
447    fn unsigned_integers_cover_extrema_overflow_and_java_ranges() {
448        for value in [0, 1, u32::MAX] {
449            let text = value.to_string();
450            assert_eq!(u32::decode(&text, context(HeaderValueKind::U32, None)).unwrap(), value);
451            assert_encoding(value, &text);
452        }
453        for value in [0, 1, u64::MAX] {
454            let text = value.to_string();
455            assert_eq!(u64::decode(&text, context(HeaderValueKind::U64, None)).unwrap(), value);
456            assert_encoding(value, &text);
457        }
458
459        assert!(matches!(
460            u32::decode("4294967296", context(HeaderValueKind::U32, None)),
461            Err(ProtocolContractViolation::InvalidValue { .. })
462        ));
463        assert!(matches!(
464            u64::decode("18446744073709551616", context(HeaderValueKind::U64, None)),
465            Err(ProtocolContractViolation::InvalidValue { .. })
466        ));
467        assert!(matches!(
468            u64::decode("-1", context(HeaderValueKind::U64, None)),
469            Err(ProtocolContractViolation::InvalidValue { .. })
470        ));
471
472        assert_eq!(
473            u32::decode(
474                &i32::MAX.to_string(),
475                context(HeaderValueKind::U32, Some(HeaderRange::I32)),
476            )
477            .unwrap(),
478            i32::MAX as u32
479        );
480        assert!(matches!(
481            u32::decode(
482                &(i32::MAX as u32 + 1).to_string(),
483                context(HeaderValueKind::U32, Some(HeaderRange::I32)),
484            ),
485            Err(ProtocolContractViolation::JavaRange { .. })
486        ));
487        assert_eq!(
488            u64::decode(
489                &i64::MAX.to_string(),
490                context(HeaderValueKind::U64, Some(HeaderRange::I64)),
491            )
492            .unwrap(),
493            i64::MAX as u64
494        );
495        assert!(matches!(
496            u64::decode(
497                &(i64::MAX as u64 + 1).to_string(),
498                context(HeaderValueKind::U64, Some(HeaderRange::I64)),
499            ),
500            Err(ProtocolContractViolation::JavaRange { .. })
501        ));
502    }
503
504    #[test]
505    fn encode_range_helper_keeps_rust_extensions_unrestricted() {
506        let unrestricted = context(HeaderValueKind::U64, None);
507        assert!(validate_unsigned_java_range(u64::MAX, unrestricted).is_ok());
508
509        let java_long = context(HeaderValueKind::U32, Some(HeaderRange::I64));
510        assert!(validate_unsigned_java_range(u32::MAX as u64, java_long).is_ok());
511
512        let java_int = context(HeaderValueKind::U32, Some(HeaderRange::I32));
513        let error = validate_unsigned_java_range(i32::MAX as u64 + 1, java_int).unwrap_err();
514        assert!(matches!(error, ProtocolContractViolation::JavaRange { .. }));
515    }
516
517    #[test]
518    fn booleans_decode_case_insensitively_but_encode_canonically() {
519        for raw in ["true", "TRUE", "TrUe"] {
520            assert!(bool::decode(raw, context(HeaderValueKind::Bool, None)).unwrap());
521        }
522        for raw in ["false", "FALSE", "FaLsE"] {
523            assert!(!bool::decode(raw, context(HeaderValueKind::Bool, None)).unwrap());
524        }
525        for raw in ["", "1", "yes", " true"] {
526            assert!(matches!(
527                bool::decode(raw, context(HeaderValueKind::Bool, None)),
528                Err(ProtocolContractViolation::InvalidValue { .. })
529            ));
530        }
531        assert_encoding(true, "true");
532        assert_encoding(false, "false");
533    }
534
535    #[test]
536    fn boundary_type_preserves_java_fallback_and_canonical_encoding() {
537        assert_eq!(
538            BoundaryType::decode("upper", context(HeaderValueKind::BoundaryType, None)).unwrap(),
539            BoundaryType::Upper
540        );
541        for raw in ["lower", "unknown", "", "上界"] {
542            assert_eq!(
543                BoundaryType::decode(raw, context(HeaderValueKind::BoundaryType, None)).unwrap(),
544                BoundaryType::Lower
545            );
546        }
547        assert_encoding(BoundaryType::Lower, "LOWER");
548        assert_encoding(BoundaryType::Upper, "UPPER");
549    }
550
551    #[test]
552    fn errors_do_not_echo_malformed_values() {
553        let raw = "secret-invalid-value";
554        let error = bool::decode(raw, context(HeaderValueKind::Bool, None)).unwrap_err();
555        let rendered = error.to_string();
556
557        assert!(!rendered.contains(raw));
558        assert_eq!(rendered, "invalid header field ExampleHeader.value: expected Bool");
559    }
560}