Skip to main content

xdr_common/
bytesm.rs

1#![allow(unused_imports)]
2#![allow(dead_code)]
3
4use crate::cfg::*;
5pub use crate::error::*;
6
7use core::fmt;
8use core::marker::PhantomData;
9use core::ops::Deref;
10
11/// A byte vector with a maximum length constraint.
12///
13/// `BytesM<MAX>` is a wrapper around `Vec<u8>` that enforces a maximum length
14/// of `MAX` bytes. This type provides hex encoding/decoding via `Display` and
15/// `FromStr` implementations.
16///
17/// This type corresponds to XDR variable-length opaque data as defined in
18/// [RFC 4506 Section 4.10](https://www.rfc-editor.org/rfc/rfc4506#section-4.10):
19/// ```text
20/// opaque identifier<m>;
21/// opaque identifier<>;
22/// ```
23///
24/// # Feature Variants
25///
26/// - **With `alloc` feature**: `Vec<u8>` is heap-allocated, supporting dynamic data
27/// - **Without `alloc` feature**: `Vec<u8>` is `&'static [u8]`, for static data only
28///
29/// # Display Format
30///
31/// When displayed or serialized (with `serde` feature), bytes are encoded as
32/// lowercase hexadecimal strings. Parsing accepts hex strings (requires `alloc`).
33///
34/// # Type Parameters
35///
36/// - `MAX` - Maximum number of bytes (defaults to `u32::MAX`)
37///
38/// # Examples
39///
40/// ```ignore
41/// use xdr_common::BytesM;
42///
43/// // Create bytes with max length 100 (e.g., "opaque hash<100>")
44/// let b: BytesM<100> = vec![0xde, 0xad, 0xbe, 0xef].try_into()?;
45/// assert_eq!(b.to_string(), "deadbeef");
46///
47/// // Parse from hex string (requires alloc feature)
48/// let parsed: BytesM<100> = "0123456789abcdef".parse()?;
49/// assert_eq!(parsed.len(), 8);
50/// ```
51#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
52pub struct BytesM<const MAX: u32 = { u32::MAX }>(pub Vec<u8>);
53
54#[cfg(all(feature = "serde", feature = "alloc"))]
55#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "alloc"))))]
56impl<'de, const MAX: u32> Deserialize<'de> for BytesM<MAX> {
57    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
58    where
59        D: Deserializer<'de>,
60    {
61        let s = String::deserialize(deserializer)?;
62        s.parse().map_err(DeError::custom)
63    }
64}
65
66#[cfg(all(feature = "serde", feature = "alloc"))]
67#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "alloc"))))]
68impl<const MAX: u32> Serialize for BytesM<MAX> {
69    fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        if self.0.len() > Self::MAX_LEN {
74            return Err(<S::Error as SerError>::custom(Error::LengthExceedsMax));
75        }
76        ser.collect_str(self)
77    }
78}
79
80impl<const MAX: u32> core::fmt::Display for BytesM<MAX> {
81    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82        #[cfg(feature = "alloc")]
83        let v = &self.0;
84        #[cfg(not(feature = "alloc"))]
85        let v = self.0;
86        for b in v {
87            write!(f, "{b:02x}")?;
88        }
89        Ok(())
90    }
91}
92
93#[cfg(feature = "alloc")]
94#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
95impl<const MAX: u32> core::str::FromStr for BytesM<MAX> {
96    type Err = Error;
97    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
98        if !s.len().is_multiple_of(2) {
99            return Err(Error::InvalidHex);
100        }
101        if s.len() / 2 > MAX as usize {
102            return Err(Error::LengthExceedsMax);
103        }
104        let mut out = Vec::with_capacity(s.len() / 2);
105        let (pairs, rest) = s.as_bytes().as_chunks::<2>();
106        debug_assert!(rest.is_empty()); // length is even, checked above
107        for pair in pairs {
108            out.push((hex_digit(pair[0])? << 4) | hex_digit(pair[1])?);
109        }
110        out.try_into()
111    }
112}
113
114/// Decode a single ASCII hex digit (`0-9`, `a-f`, `A-F`) to its 0-15 value.
115#[cfg(feature = "alloc")]
116pub(crate) fn hex_digit(b: u8) -> core::result::Result<u8, Error> {
117    match b {
118        b'0'..=b'9' => Ok(b - b'0'),
119        b'a'..=b'f' => Ok(b - b'a' + 10),
120        b'A'..=b'F' => Ok(b - b'A' + 10),
121        _ => Err(Error::InvalidHex),
122    }
123}
124
125impl<const MAX: u32> core::fmt::Debug for BytesM<MAX> {
126    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
127        #[cfg(feature = "alloc")]
128        let v = &self.0;
129        #[cfg(not(feature = "alloc"))]
130        let v = self.0;
131        write!(f, "BytesM(")?;
132        for b in v {
133            write!(f, "{b:02x}")?;
134        }
135        write!(f, ")")?;
136        Ok(())
137    }
138}
139
140#[cfg(feature = "schemars")]
141#[cfg_attr(docsrs, doc(cfg(feature = "schemars")))]
142impl<const MAX: u32> schemars::JsonSchema for BytesM<MAX> {
143    fn schema_name() -> alloc::borrow::Cow<'static, str> {
144        alloc::format!("BytesM<{}>", MAX).into()
145    }
146
147    fn schema_id() -> alloc::borrow::Cow<'static, str> {
148        alloc::format!("xdr_common::BytesM<{}>", MAX).into()
149    }
150
151    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
152        schemars::json_schema!({
153            "type": "string",
154            "maxLength": u64::from(MAX) * 2,
155            "contentEncoding": "hex",
156            "contentMediaType": "application/binary",
157            "pattern": "^([0-9a-fA-F]{2})*$",
158        })
159    }
160}
161
162impl<const MAX: u32> Deref for BytesM<MAX> {
163    type Target = Vec<u8>;
164
165    fn deref(&self) -> &Self::Target {
166        &self.0
167    }
168}
169
170impl<const MAX: u32> Default for BytesM<MAX> {
171    fn default() -> Self {
172        Self(Vec::default())
173    }
174}
175
176impl<const MAX: u32> BytesM<MAX> {
177    /// The maximum length as a compile-time constant.
178    pub const MAX_LEN: usize = { MAX as usize };
179
180    /// Returns the maximum allowed length for this byte vector.
181    #[must_use]
182    #[allow(clippy::unused_self)]
183    pub fn max_len(&self) -> usize {
184        Self::MAX_LEN
185    }
186
187    /// Returns a reference to the underlying byte vector.
188    #[must_use]
189    pub fn as_vec(&self) -> &Vec<u8> {
190        self.as_ref()
191    }
192
193    /// Returns the bytes as a slice.
194    ///
195    /// Usable in `const` contexts.
196    #[must_use]
197    pub const fn as_slice(&self) -> &[u8] {
198        #[cfg(feature = "alloc")]
199        let s: &[u8] = self.0.as_slice();
200        #[cfg(not(feature = "alloc"))]
201        let s: &[u8] = self.0;
202        s
203    }
204}
205
206impl<const MAX: u32> BytesM<MAX> {
207    /// Clones the bytes and returns them as a standard `Vec<u8>`.
208    ///
209    /// Requires the `alloc` feature.
210    #[must_use]
211    #[cfg(feature = "alloc")]
212    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
213    pub fn to_vec(&self) -> Vec<u8> {
214        self.into()
215    }
216
217    /// Consumes `self` and returns the underlying byte vector.
218    #[must_use]
219    pub fn into_vec(self) -> Vec<u8> {
220        self.into()
221    }
222}
223
224impl<const MAX: u32> TryFrom<Vec<u8>> for BytesM<MAX> {
225    type Error = Error;
226
227    fn try_from(v: Vec<u8>) -> Result<Self, Error> {
228        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
229        if len <= MAX {
230            Ok(BytesM(v))
231        } else {
232            Err(Error::LengthExceedsMax)
233        }
234    }
235}
236
237impl<const MAX: u32> From<BytesM<MAX>> for Vec<u8> {
238    fn from(v: BytesM<MAX>) -> Self {
239        v.0
240    }
241}
242
243#[cfg(feature = "alloc")]
244#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
245impl<const MAX: u32> From<&BytesM<MAX>> for Vec<u8> {
246    fn from(v: &BytesM<MAX>) -> Self {
247        v.0.clone()
248    }
249}
250
251impl<const MAX: u32> AsRef<Vec<u8>> for BytesM<MAX> {
252    fn as_ref(&self) -> &Vec<u8> {
253        &self.0
254    }
255}
256
257#[cfg(feature = "alloc")]
258#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
259impl<const MAX: u32> TryFrom<&Vec<u8>> for BytesM<MAX> {
260    type Error = Error;
261
262    fn try_from(v: &Vec<u8>) -> Result<Self, Error> {
263        v.as_slice().try_into()
264    }
265}
266
267#[cfg(feature = "alloc")]
268#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
269impl<const MAX: u32> TryFrom<&[u8]> for BytesM<MAX> {
270    type Error = Error;
271
272    fn try_from(v: &[u8]) -> Result<Self, Error> {
273        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
274        if len <= MAX {
275            Ok(BytesM(v.to_vec()))
276        } else {
277            Err(Error::LengthExceedsMax)
278        }
279    }
280}
281
282impl<const MAX: u32> AsRef<[u8]> for BytesM<MAX> {
283    #[cfg(feature = "alloc")]
284    fn as_ref(&self) -> &[u8] {
285        self.0.as_ref()
286    }
287    #[cfg(not(feature = "alloc"))]
288    fn as_ref(&self) -> &[u8] {
289        self.0
290    }
291}
292
293#[cfg(feature = "alloc")]
294#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
295impl<const N: usize, const MAX: u32> TryFrom<[u8; N]> for BytesM<MAX> {
296    type Error = Error;
297
298    fn try_from(v: [u8; N]) -> Result<Self, Error> {
299        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
300        if len <= MAX {
301            Ok(BytesM(v.to_vec()))
302        } else {
303            Err(Error::LengthExceedsMax)
304        }
305    }
306}
307
308#[cfg(feature = "alloc")]
309#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
310impl<const N: usize, const MAX: u32> TryFrom<BytesM<MAX>> for [u8; N] {
311    type Error = BytesM<MAX>;
312
313    fn try_from(v: BytesM<MAX>) -> core::result::Result<Self, Self::Error> {
314        let s: [u8; N] = v.0.try_into().map_err(BytesM::<MAX>)?;
315        Ok(s)
316    }
317}
318
319#[cfg(feature = "alloc")]
320#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
321impl<const N: usize, const MAX: u32> TryFrom<&[u8; N]> for BytesM<MAX> {
322    type Error = Error;
323
324    fn try_from(v: &[u8; N]) -> Result<Self, Error> {
325        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
326        if len <= MAX {
327            Ok(BytesM(v.to_vec()))
328        } else {
329            Err(Error::LengthExceedsMax)
330        }
331    }
332}
333
334#[cfg(not(feature = "alloc"))]
335#[cfg_attr(docsrs, doc(cfg(not(feature = "alloc"))))]
336impl<const N: usize, const MAX: u32> TryFrom<&'static [u8; N]> for BytesM<MAX> {
337    type Error = Error;
338
339    fn try_from(v: &'static [u8; N]) -> Result<Self, Error> {
340        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
341        if len <= MAX {
342            Ok(BytesM(v))
343        } else {
344            Err(Error::LengthExceedsMax)
345        }
346    }
347}
348
349#[cfg(feature = "alloc")]
350#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
351impl<const MAX: u32> TryFrom<&String> for BytesM<MAX> {
352    type Error = Error;
353
354    fn try_from(v: &String) -> Result<Self, Error> {
355        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
356        if len <= MAX {
357            Ok(BytesM(v.as_bytes().to_vec()))
358        } else {
359            Err(Error::LengthExceedsMax)
360        }
361    }
362}
363
364#[cfg(feature = "alloc")]
365#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
366impl<const MAX: u32> TryFrom<String> for BytesM<MAX> {
367    type Error = Error;
368
369    fn try_from(v: String) -> Result<Self, Error> {
370        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
371        if len <= MAX {
372            Ok(BytesM(v.into()))
373        } else {
374            Err(Error::LengthExceedsMax)
375        }
376    }
377}
378
379#[cfg(feature = "alloc")]
380#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
381impl<const MAX: u32> TryFrom<&str> for BytesM<MAX> {
382    type Error = Error;
383
384    fn try_from(v: &str) -> Result<Self, Error> {
385        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
386        if len <= MAX {
387            Ok(BytesM(v.into()))
388        } else {
389            Err(Error::LengthExceedsMax)
390        }
391    }
392}
393
394#[cfg(not(feature = "alloc"))]
395#[cfg_attr(docsrs, doc(cfg(not(feature = "alloc"))))]
396impl<const MAX: u32> TryFrom<&'static str> for BytesM<MAX> {
397    type Error = Error;
398
399    fn try_from(v: &'static str) -> Result<Self, Error> {
400        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
401        if len <= MAX {
402            Ok(BytesM(v.as_bytes()))
403        } else {
404            Err(Error::LengthExceedsMax)
405        }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[cfg(feature = "alloc")]
414    extern crate alloc;
415    #[cfg(feature = "alloc")]
416    use alloc::{string::ToString, vec};
417
418    #[test]
419    #[cfg(feature = "alloc")]
420    fn test_as_slice_const() {
421        // Compile check that as_slice is const-callable. With alloc, BytesM
422        // values cannot be used inside const eval (Vec's destructor is not
423        // const), so the const fn is exercised at runtime.
424        const fn as_slice_in_const<const M: u32>(b: &BytesM<M>) -> &[u8] {
425            b.as_slice()
426        }
427        let b: BytesM<3> = vec![0xde, 0xad].try_into().unwrap();
428        assert_eq!(as_slice_in_const(&b), &[0xde, 0xad]);
429    }
430
431    #[test]
432    #[cfg(not(feature = "alloc"))]
433    fn test_as_slice_const() {
434        const B: BytesM<3> = BytesM(&[0xde, 0xad]);
435        const SLICE: &[u8] = B.as_slice();
436        assert_eq!(SLICE, &[0xde, 0xad]);
437    }
438
439    #[test]
440    #[cfg(feature = "alloc")]
441    fn test_bytesm_alloc_basic() {
442        // Test basic creation with alloc feature
443        let b: BytesM<100> = vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap();
444        assert_eq!(b.len(), 4);
445        assert_eq!(b.max_len(), 100);
446        assert_eq!(b.to_string(), "deadbeef");
447
448        // Test length exceeds max
449        let too_long: Result<BytesM<3>, _> = vec![1, 2, 3, 4].try_into();
450        assert!(too_long.is_err());
451    }
452
453    #[test]
454    #[cfg(feature = "alloc")]
455    fn from_str_rejects_odd_length_hex() {
456        let result = "abc".parse::<BytesM<100>>();
457        assert_eq!(result, Err(Error::InvalidHex));
458    }
459
460    #[test]
461    #[cfg(feature = "alloc")]
462    fn from_str_rejects_overlength_before_decode() {
463        // 6 hex chars = 3 decoded bytes, which exceeds MAX=2.
464        // Must return LengthExceedsMax without allocating the decoded vec.
465        let input = "aabbcc";
466        let result = input.parse::<BytesM<2>>();
467        assert_eq!(result, Err(Error::LengthExceedsMax));
468    }
469
470    #[test]
471    #[cfg(feature = "alloc")]
472    fn from_str_accepts_valid_hex_within_bound() {
473        let result = "deadbeef".parse::<BytesM<4>>();
474        assert!(result.is_ok());
475        assert_eq!(
476            AsRef::<[u8]>::as_ref(&result.unwrap()),
477            &[0xde, 0xad, 0xbe, 0xef]
478        );
479    }
480
481    #[test]
482    #[cfg(feature = "alloc")]
483    fn from_str_accepts_exact_max_length() {
484        // 4 hex chars = 2 decoded bytes = exactly MAX
485        let result = "aabb".parse::<BytesM<2>>();
486        assert!(result.is_ok());
487        assert_eq!(result.unwrap().len(), 2);
488    }
489
490    #[test]
491    #[cfg(feature = "alloc")]
492    fn from_str_rejects_invalid_hex_chars() {
493        let result = "zzzz".parse::<BytesM<100>>();
494        assert_eq!(result, Err(Error::InvalidHex));
495    }
496
497    #[test]
498    #[cfg(feature = "alloc")]
499    fn from_str_overlength_rejects_before_validating_hex_content() {
500        // Input is overlength AND contains invalid hex chars.
501        // The pre-decode length guard should reject on length first,
502        // without attempting to decode and discover the invalid chars.
503        let result = "aabbccZZ".parse::<BytesM<1>>();
504        assert_eq!(result, Err(Error::LengthExceedsMax));
505    }
506
507    #[test]
508    #[cfg(feature = "schemars")]
509    fn schema_pattern_rejects_odd_length_hex() {
510        let schema = schemars::schema_for!(BytesM<2>);
511        let obj = schema
512            .as_value()
513            .as_object()
514            .expect("schema should be a JSON object");
515        let pattern = obj
516            .get("pattern")
517            .expect("schema missing pattern key")
518            .as_str()
519            .expect("pattern should be a string");
520        let re = regex_lite::Regex::new(pattern).unwrap();
521        assert!(re.is_match("aabb"), "pattern should accept even-length hex");
522        assert!(re.is_match(""), "pattern should accept empty string");
523        assert!(!re.is_match("f"), "pattern must reject odd-length hex");
524        assert!(!re.is_match("abc"), "pattern must reject odd-length hex");
525    }
526
527    #[test]
528    #[cfg(all(feature = "serde", feature = "alloc"))]
529    fn test_bytesm_serde_serialize_rejects_overlimit() {
530        let value = BytesM::<1>(vec![0xaa, 0xbb]);
531        let err = serde_json::to_value(&value).unwrap_err();
532        let msg = alloc::format!("{err}");
533        assert!(msg.contains("max length exceeded"), "{msg}");
534    }
535
536    #[test]
537    #[cfg(all(feature = "serde", feature = "alloc"))]
538    fn test_bytesm_serde_serialize_accepts_at_max() {
539        let value = BytesM::<2>(vec![0xaa, 0xbb]);
540        let json = serde_json::to_value(&value).unwrap();
541        assert_eq!(json, serde_json::json!("aabb"));
542    }
543
544    #[test]
545    #[cfg(all(feature = "serde", feature = "alloc"))]
546    fn test_bytesm_serde_roundtrip() {
547        let value = BytesM::<100>(vec![0xde, 0xad, 0xbe, 0xef]);
548        let json = serde_json::to_value(&value).unwrap();
549        let back: BytesM<100> = serde_json::from_value(json).unwrap();
550        assert_eq!(value, back);
551    }
552
553    #[test]
554    #[cfg(all(feature = "serde", feature = "alloc"))]
555    fn test_bytesm_serde_deserialize_rejects_invalid_hex() {
556        let err = serde_json::from_value::<BytesM<100>>(serde_json::json!("zz"));
557        assert!(err.is_err());
558    }
559
560    #[test]
561    #[cfg(all(feature = "serde", feature = "alloc"))]
562    fn test_bytesm_serde_deserialize_rejects_overlimit() {
563        let err = serde_json::from_value::<BytesM<1>>(serde_json::json!("aabb"));
564        assert!(err.is_err());
565    }
566
567    #[test]
568    #[cfg(not(feature = "alloc"))]
569    fn test_bytesm_noalloc_basic() {
570        // Test basic creation without alloc feature (static data only)
571        static DATA: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
572        let b: BytesM<100> = (&DATA).try_into().unwrap();
573        assert_eq!(b.len(), 4);
574        assert_eq!(b.max_len(), 100);
575        assert_eq!(AsRef::<[u8]>::as_ref(&b), &[0xde, 0xad, 0xbe, 0xef]);
576
577        // Test length exceeds max
578        static LONG_DATA: [u8; 4] = [1, 2, 3, 4];
579        let too_long: Result<BytesM<3>, _> = (&LONG_DATA).try_into();
580        assert!(too_long.is_err());
581    }
582}