Skip to main content

xdr_common/
stringm.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 string type that stores arbitrary bytes with a maximum length constraint.
12///
13/// `StringM<MAX>` is a wrapper around `Vec<u8>` that represents string data,
14/// which may or may not be valid UTF-8. This type enforces a maximum length
15/// of `MAX` bytes.
16///
17/// This type corresponds to XDR strings as defined in
18/// [RFC 4506 Section 4.11](https://www.rfc-editor.org/rfc/rfc4506#section-4.11):
19/// ```text
20/// string identifier<m>;
21/// string identifier<>;
22/// ```
23///
24/// # Character Encoding
25///
26/// **Note:** The XDR standard (RFC 4506) specifies that strings should contain
27/// ASCII characters. However, this implementation does **not** enforce ASCII
28/// encoding, nor does it strictly enforce UTF-8. The underlying storage is a
29/// raw byte vector (`Vec<u8>`) that accepts arbitrary bytes. Users are
30/// responsible for ensuring their data meets the requirements of their specific
31/// XDR protocol.
32///
33/// # UTF-8 Conversion
34///
35/// - Fallible conversion to/from UTF-8 `String`: [`TryFrom`]/[`TryInto`]/[`StringM::to_utf8_string`]
36/// - Lossy conversion to UTF-8 `String`: [`StringM::to_utf8_string_lossy`]
37///
38/// # Feature Variants
39///
40/// - **With `alloc` feature**: `Vec<u8>` is heap-allocated, supporting dynamic data
41/// - **Without `alloc` feature**: `Vec<u8>` is `&'static [u8]`, for static data only
42///
43/// # Display Format and Serde Representation
44///
45/// When displayed or serialized (with `serde` feature), strings are shown
46/// as escaped printable ASCII using [`core::fmt::Display`]/`ToString`. Non-ASCII
47/// bytes are hex-escaped (e.g. `\xc3\xa9` for `Γ©`).
48///
49/// Deserialization via [`core::str::FromStr`] is intentionally permissive: it
50/// accepts both the canonical escaped form and raw UTF-8 input. This means
51/// `"cafΓ©"` and `"caf\xc3\xa9"` both deserialize to the same byte sequence,
52/// but serialization always produces the escaped form.
53///
54/// # Type Parameters
55///
56/// - `MAX` - Maximum number of bytes (defaults to `u32::MAX`)
57///
58/// # Examples
59///
60/// ```ignore
61/// use xdr_common::StringM;
62///
63/// // Create a string with max length 50 (e.g., "string name<50>")
64/// let s: StringM<50> = "hello".try_into()?;
65/// assert_eq!(s.to_utf8_string()?, "hello");
66///
67/// // Handle arbitrary bytes (may not be valid UTF-8)
68/// let bytes: StringM<100> = vec![0xFF, 0xFE].try_into()?;
69/// let lossy = bytes.to_utf8_string_lossy(); // Replaces invalid chars
70/// ```
71#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
72pub struct StringM<const MAX: u32 = { u32::MAX }>(pub Vec<u8>);
73
74#[cfg(all(feature = "serde", feature = "alloc"))]
75#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "alloc"))))]
76impl<'de, const MAX: u32> Deserialize<'de> for StringM<MAX> {
77    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        let s = String::deserialize(deserializer)?;
82        s.parse().map_err(DeError::custom)
83    }
84}
85
86#[cfg(all(feature = "serde", feature = "alloc"))]
87#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "alloc"))))]
88impl<const MAX: u32> Serialize for StringM<MAX> {
89    fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
90    where
91        S: Serializer,
92    {
93        if self.0.len() > Self::MAX_LEN {
94            return Err(<S::Error as SerError>::custom(Error::LengthExceedsMax));
95        }
96        ser.collect_str(self)
97    }
98}
99
100impl<const MAX: u32> core::fmt::Display for StringM<MAX> {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        #[cfg(feature = "alloc")]
103        let v = &self.0;
104        #[cfg(not(feature = "alloc"))]
105        let v = self.0;
106        for b in escape_bytes::Escape::new(v) {
107            write!(f, "{}", b as char)?;
108        }
109        Ok(())
110    }
111}
112
113impl<const MAX: u32> core::fmt::Debug for StringM<MAX> {
114    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115        #[cfg(feature = "alloc")]
116        let v = &self.0;
117        #[cfg(not(feature = "alloc"))]
118        let v = self.0;
119        write!(f, "StringM(")?;
120        for b in escape_bytes::Escape::new(v) {
121            write!(f, "{}", b as char)?;
122        }
123        write!(f, ")")?;
124        Ok(())
125    }
126}
127
128#[cfg(feature = "alloc")]
129#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
130impl<const MAX: u32> core::str::FromStr for StringM<MAX> {
131    type Err = Error;
132    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
133        // Each escaped byte is at most 4 input chars (\xNN), so if the input
134        // exceeds MAX * 4, the unescaped output will certainly exceed MAX.
135        if s.len() > Self::MAX_LEN.saturating_mul(4) {
136            return Err(Error::LengthExceedsMax);
137        }
138        escape_bytes::unescape(s.as_bytes())
139            .map_err(|_| Error::Invalid)?
140            .try_into()
141    }
142}
143
144#[cfg(feature = "schemars")]
145#[cfg_attr(docsrs, doc(cfg(feature = "schemars")))]
146impl<const MAX: u32> schemars::JsonSchema for StringM<MAX> {
147    fn schema_name() -> alloc::borrow::Cow<'static, str> {
148        alloc::format!("StringM<{}>", MAX).into()
149    }
150
151    fn schema_id() -> alloc::borrow::Cow<'static, str> {
152        alloc::format!("xdr_common::StringM<{}>", MAX).into()
153    }
154
155    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
156        schemars::json_schema!({
157            "type": "string",
158            "x-max-unescaped-bytes": MAX,
159            "description": "XDR string with escape_bytes encoding. x-max-unescaped-bytes is the byte limit of the unescaped XDR payload, not the character count of the JSON value.",
160        })
161    }
162}
163
164impl<const MAX: u32> Deref for StringM<MAX> {
165    type Target = Vec<u8>;
166
167    fn deref(&self) -> &Self::Target {
168        &self.0
169    }
170}
171
172impl<const MAX: u32> Default for StringM<MAX> {
173    fn default() -> Self {
174        Self(Vec::default())
175    }
176}
177
178impl<const MAX: u32> StringM<MAX> {
179    /// The maximum length as a compile-time constant.
180    pub const MAX_LEN: usize = { MAX as usize };
181
182    /// Returns the maximum allowed length for this string.
183    #[must_use]
184    #[allow(clippy::unused_self)]
185    pub fn max_len(&self) -> usize {
186        Self::MAX_LEN
187    }
188
189    /// Returns a reference to the underlying byte vector.
190    #[must_use]
191    pub fn as_vec(&self) -> &Vec<u8> {
192        self.as_ref()
193    }
194
195    /// Returns the string's bytes as a slice.
196    ///
197    /// Usable in `const` contexts.
198    #[must_use]
199    pub const fn as_slice(&self) -> &[u8] {
200        #[cfg(feature = "alloc")]
201        let s: &[u8] = self.0.as_slice();
202        #[cfg(not(feature = "alloc"))]
203        let s: &[u8] = self.0;
204        s
205    }
206}
207
208impl<const MAX: u32> StringM<MAX> {
209    /// Clones the bytes and returns them as a standard `Vec<u8>`.
210    ///
211    /// Requires the `alloc` feature.
212    #[must_use]
213    #[cfg(feature = "alloc")]
214    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
215    pub fn to_vec(&self) -> Vec<u8> {
216        self.into()
217    }
218
219    /// Consumes `self` and returns the underlying byte vector.
220    #[must_use]
221    pub fn into_vec(self) -> Vec<u8> {
222        self.into()
223    }
224}
225
226impl<const MAX: u32> StringM<MAX> {
227    /// Converts the string to a UTF-8 `String`.
228    ///
229    /// Returns [`Error::InvalidUTF8`] if the bytes are not valid UTF-8.
230    ///
231    /// Requires the `alloc` feature.
232    #[cfg(feature = "alloc")]
233    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
234    pub fn to_utf8_string(&self) -> Result<String, Error> {
235        self.try_into()
236    }
237
238    /// Consumes `self` and converts it to a UTF-8 `String`.
239    ///
240    /// Returns [`Error::InvalidUTF8`] if the bytes are not valid UTF-8.
241    ///
242    /// Requires the `alloc` feature.
243    #[cfg(feature = "alloc")]
244    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
245    pub fn into_utf8_string(self) -> Result<String, Error> {
246        self.try_into()
247    }
248
249    /// Converts the string to a UTF-8 `String`, replacing invalid sequences.
250    ///
251    /// Invalid UTF-8 sequences are replaced with the Unicode replacement character (U+FFFD).
252    ///
253    /// Requires the `alloc` feature.
254    #[cfg(feature = "alloc")]
255    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
256    #[must_use]
257    pub fn to_utf8_string_lossy(&self) -> String {
258        String::from_utf8_lossy(&self.0).into_owned()
259    }
260
261    /// Consumes `self` and converts it to a UTF-8 `String`, replacing invalid sequences.
262    ///
263    /// Invalid UTF-8 sequences are replaced with the Unicode replacement character (U+FFFD).
264    ///
265    /// Requires the `alloc` feature.
266    #[cfg(feature = "alloc")]
267    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
268    #[must_use]
269    pub fn into_utf8_string_lossy(self) -> String {
270        String::from_utf8_lossy(&self.0).into_owned()
271    }
272}
273
274impl<const MAX: u32> TryFrom<Vec<u8>> for StringM<MAX> {
275    type Error = Error;
276
277    fn try_from(v: Vec<u8>) -> Result<Self, Error> {
278        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
279        if len <= MAX {
280            Ok(StringM(v))
281        } else {
282            Err(Error::LengthExceedsMax)
283        }
284    }
285}
286
287impl<const MAX: u32> From<StringM<MAX>> for Vec<u8> {
288    fn from(v: StringM<MAX>) -> Self {
289        v.0
290    }
291}
292
293#[cfg(feature = "alloc")]
294#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
295impl<const MAX: u32> From<&StringM<MAX>> for Vec<u8> {
296    fn from(v: &StringM<MAX>) -> Self {
297        v.0.clone()
298    }
299}
300
301impl<const MAX: u32> AsRef<Vec<u8>> for StringM<MAX> {
302    fn as_ref(&self) -> &Vec<u8> {
303        &self.0
304    }
305}
306
307#[cfg(feature = "alloc")]
308#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
309impl<const MAX: u32> TryFrom<&Vec<u8>> for StringM<MAX> {
310    type Error = Error;
311
312    fn try_from(v: &Vec<u8>) -> Result<Self, Error> {
313        v.as_slice().try_into()
314    }
315}
316
317#[cfg(feature = "alloc")]
318#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
319impl<const MAX: u32> TryFrom<&[u8]> for StringM<MAX> {
320    type Error = Error;
321
322    fn try_from(v: &[u8]) -> Result<Self, Error> {
323        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
324        if len <= MAX {
325            Ok(StringM(v.to_vec()))
326        } else {
327            Err(Error::LengthExceedsMax)
328        }
329    }
330}
331
332impl<const MAX: u32> AsRef<[u8]> for StringM<MAX> {
333    #[cfg(feature = "alloc")]
334    fn as_ref(&self) -> &[u8] {
335        self.0.as_ref()
336    }
337    #[cfg(not(feature = "alloc"))]
338    fn as_ref(&self) -> &[u8] {
339        self.0
340    }
341}
342
343#[cfg(feature = "alloc")]
344#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
345impl<const N: usize, const MAX: u32> TryFrom<[u8; N]> for StringM<MAX> {
346    type Error = Error;
347
348    fn try_from(v: [u8; N]) -> Result<Self, Error> {
349        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
350        if len <= MAX {
351            Ok(StringM(v.to_vec()))
352        } else {
353            Err(Error::LengthExceedsMax)
354        }
355    }
356}
357
358#[cfg(feature = "alloc")]
359#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
360impl<const N: usize, const MAX: u32> TryFrom<StringM<MAX>> for [u8; N] {
361    type Error = StringM<MAX>;
362
363    fn try_from(v: StringM<MAX>) -> core::result::Result<Self, Self::Error> {
364        let s: [u8; N] = v.0.try_into().map_err(StringM::<MAX>)?;
365        Ok(s)
366    }
367}
368
369#[cfg(feature = "alloc")]
370#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
371impl<const N: usize, const MAX: u32> TryFrom<&[u8; N]> for StringM<MAX> {
372    type Error = Error;
373
374    fn try_from(v: &[u8; N]) -> Result<Self, Error> {
375        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
376        if len <= MAX {
377            Ok(StringM(v.to_vec()))
378        } else {
379            Err(Error::LengthExceedsMax)
380        }
381    }
382}
383
384#[cfg(not(feature = "alloc"))]
385#[cfg_attr(docsrs, doc(cfg(not(feature = "alloc"))))]
386impl<const N: usize, const MAX: u32> TryFrom<&'static [u8; N]> for StringM<MAX> {
387    type Error = Error;
388
389    fn try_from(v: &'static [u8; N]) -> Result<Self, Error> {
390        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
391        if len <= MAX {
392            Ok(StringM(v))
393        } else {
394            Err(Error::LengthExceedsMax)
395        }
396    }
397}
398
399#[cfg(feature = "alloc")]
400#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
401impl<const MAX: u32> TryFrom<&String> for StringM<MAX> {
402    type Error = Error;
403
404    fn try_from(v: &String) -> Result<Self, Error> {
405        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
406        if len <= MAX {
407            Ok(StringM(v.as_bytes().to_vec()))
408        } else {
409            Err(Error::LengthExceedsMax)
410        }
411    }
412}
413
414#[cfg(feature = "alloc")]
415#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
416impl<const MAX: u32> TryFrom<String> for StringM<MAX> {
417    type Error = Error;
418
419    fn try_from(v: String) -> Result<Self, Error> {
420        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
421        if len <= MAX {
422            Ok(StringM(v.into()))
423        } else {
424            Err(Error::LengthExceedsMax)
425        }
426    }
427}
428
429#[cfg(feature = "alloc")]
430#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
431impl<const MAX: u32> TryFrom<StringM<MAX>> for String {
432    type Error = Error;
433
434    fn try_from(v: StringM<MAX>) -> Result<Self, Error> {
435        Ok(String::from_utf8(v.0)?)
436    }
437}
438
439#[cfg(feature = "alloc")]
440#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
441impl<const MAX: u32> TryFrom<&StringM<MAX>> for String {
442    type Error = Error;
443
444    fn try_from(v: &StringM<MAX>) -> Result<Self, Error> {
445        Ok(core::str::from_utf8(v.as_ref())?.to_owned())
446    }
447}
448
449#[cfg(feature = "alloc")]
450#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
451impl<const MAX: u32> TryFrom<&str> for StringM<MAX> {
452    type Error = Error;
453
454    fn try_from(v: &str) -> Result<Self, Error> {
455        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
456        if len <= MAX {
457            Ok(StringM(v.into()))
458        } else {
459            Err(Error::LengthExceedsMax)
460        }
461    }
462}
463
464#[cfg(not(feature = "alloc"))]
465#[cfg_attr(docsrs, doc(cfg(not(feature = "alloc"))))]
466impl<const MAX: u32> TryFrom<&'static str> for StringM<MAX> {
467    type Error = Error;
468
469    fn try_from(v: &'static str) -> Result<Self, Error> {
470        let len: u32 = v.len().try_into().map_err(|_| Error::LengthExceedsMax)?;
471        if len <= MAX {
472            Ok(StringM(v.as_bytes()))
473        } else {
474            Err(Error::LengthExceedsMax)
475        }
476    }
477}
478
479impl<'a, const MAX: u32> TryFrom<&'a StringM<MAX>> for &'a str {
480    type Error = Error;
481
482    fn try_from(v: &'a StringM<MAX>) -> Result<Self, Error> {
483        Ok(core::str::from_utf8(v.as_ref())?)
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[cfg(feature = "alloc")]
492    extern crate alloc;
493    #[cfg(feature = "alloc")]
494    use alloc::string::ToString;
495    #[cfg(feature = "alloc")]
496    use alloc::vec;
497    #[cfg(feature = "alloc")]
498    use alloc::vec::Vec;
499
500    #[test]
501    #[cfg(feature = "alloc")]
502    fn test_as_slice_const() {
503        // Compile check that as_slice is const-callable. With alloc, StringM
504        // values cannot be used inside const eval (Vec's destructor is not
505        // const), so the const fn is exercised at runtime.
506        const fn as_slice_in_const<const M: u32>(s: &StringM<M>) -> &[u8] {
507            s.as_slice()
508        }
509        let s: StringM<100> = "xdr".try_into().unwrap();
510        assert_eq!(as_slice_in_const(&s), b"xdr");
511    }
512
513    #[test]
514    #[cfg(not(feature = "alloc"))]
515    fn test_as_slice_const() {
516        const S: StringM<8> = StringM(b"xdr");
517        const SLICE: &[u8] = S.as_slice();
518        assert_eq!(SLICE, b"xdr");
519    }
520
521    #[test]
522    #[cfg(feature = "alloc")]
523    fn test_to_utf8_string_lossy_valid_utf8() {
524        // Valid UTF-8 string should pass through unchanged
525        let s: StringM<100> = "Hello, World!".try_into().unwrap();
526        assert_eq!(s.to_utf8_string_lossy(), "Hello, World!");
527    }
528
529    #[test]
530    #[cfg(feature = "alloc")]
531    fn test_to_utf8_string_lossy_invalid_utf8() {
532        // Invalid UTF-8 bytes should be replaced with replacement character
533        let bytes = vec![0xFF, 0xFE, 0xFD];
534        let s: StringM<100> = bytes.try_into().unwrap();
535        let result = s.to_utf8_string_lossy();
536
537        // Each invalid byte should be replaced with U+FFFD
538        assert_eq!(result, "\u{FFFD}\u{FFFD}\u{FFFD}");
539    }
540
541    #[test]
542    #[cfg(feature = "alloc")]
543    fn test_to_utf8_string_lossy_mixed() {
544        // Mix of valid and invalid UTF-8
545        let mut bytes = Vec::new();
546        bytes.extend_from_slice(b"Hello");
547        bytes.push(0xFF); // Invalid UTF-8
548        bytes.extend_from_slice(b"World");
549
550        let s: StringM<100> = bytes.try_into().unwrap();
551        let result = s.to_utf8_string_lossy();
552
553        assert_eq!(result, "Hello\u{FFFD}World");
554    }
555
556    #[test]
557    #[cfg(feature = "alloc")]
558    fn test_into_utf8_string_lossy_valid_utf8() {
559        // Valid UTF-8 string should pass through unchanged
560        let s: StringM<100> = "Test String".try_into().unwrap();
561        assert_eq!(s.into_utf8_string_lossy(), "Test String");
562    }
563
564    #[test]
565    #[cfg(feature = "alloc")]
566    fn test_into_utf8_string_lossy_invalid_utf8() {
567        // Invalid UTF-8 bytes should be replaced
568        let bytes = vec![0xC0, 0xC1, 0xF5];
569        let s: StringM<100> = bytes.try_into().unwrap();
570        let result = s.into_utf8_string_lossy();
571
572        // All bytes are invalid UTF-8 starts
573        assert!(result.contains('\u{FFFD}'));
574    }
575
576    #[test]
577    #[cfg(feature = "alloc")]
578    fn test_to_utf8_string_success() {
579        // Valid UTF-8 should convert successfully
580        let s: StringM<100> = "Valid UTF-8".try_into().unwrap();
581        assert_eq!(s.to_utf8_string().unwrap(), "Valid UTF-8");
582    }
583
584    #[test]
585    #[cfg(feature = "alloc")]
586    fn test_to_utf8_string_failure() {
587        // Invalid UTF-8 should fail
588        let bytes = vec![0xFF, 0xFE];
589        let s: StringM<100> = bytes.try_into().unwrap();
590        assert!(s.to_utf8_string().is_err());
591    }
592
593    #[test]
594    #[cfg(feature = "alloc")]
595    fn test_into_utf8_string_success() {
596        // Valid UTF-8 should convert successfully
597        let s: StringM<100> = "Another test".try_into().unwrap();
598        assert_eq!(s.into_utf8_string().unwrap(), "Another test");
599    }
600
601    #[test]
602    #[cfg(feature = "alloc")]
603    fn test_into_utf8_string_failure() {
604        // Invalid UTF-8 should fail
605        let bytes = vec![0x80, 0x81];
606        let s: StringM<100> = bytes.try_into().unwrap();
607        assert!(s.into_utf8_string().is_err());
608    }
609
610    #[test]
611    fn test_max_len_constraint() {
612        // Test that length constraint is enforced
613        let short: Result<StringM<5>, _> = "hello".try_into();
614        assert!(short.is_ok());
615
616        let too_long: Result<StringM<5>, _> = "hello world".try_into();
617        assert!(too_long.is_err());
618    }
619
620    #[test]
621    #[cfg(feature = "alloc")]
622    fn test_empty_string() {
623        let s: StringM<100> = "".try_into().unwrap();
624        assert_eq!(s.to_utf8_string_lossy(), "");
625        assert_eq!(s.to_utf8_string().unwrap(), "");
626    }
627
628    #[test]
629    #[cfg(feature = "alloc")]
630    fn test_unicode_characters() {
631        // Test with various Unicode characters
632        let s: StringM<100> = "Hello δΈ–η•Œ 🌍".try_into().unwrap();
633        assert_eq!(s.to_utf8_string_lossy(), "Hello δΈ–η•Œ 🌍");
634        assert_eq!(s.to_utf8_string().unwrap(), "Hello δΈ–η•Œ 🌍");
635    }
636
637    #[test]
638    #[cfg(feature = "alloc")]
639    fn test_fromstr_rejects_overlength() {
640        let result = "abcd".parse::<StringM<3>>();
641        assert!(matches!(result, Err(Error::LengthExceedsMax)));
642    }
643
644    #[test]
645    #[cfg(feature = "alloc")]
646    fn test_fromstr_accepts_at_max() {
647        let s = "abc".parse::<StringM<3>>().unwrap();
648        assert_eq!(s.as_ref() as &[u8], b"abc");
649    }
650
651    #[test]
652    #[cfg(feature = "alloc")]
653    fn test_fromstr_rejects_overlength_escaped() {
654        let result = "\\x61\\x62\\x63\\x64".parse::<StringM<3>>();
655        assert!(matches!(result, Err(Error::LengthExceedsMax)));
656    }
657
658    #[test]
659    #[cfg(feature = "alloc")]
660    fn test_fromstr_roundtrip() {
661        let original: StringM<10> = vec![0xFF, b'a', b'b'].try_into().unwrap();
662        let displayed = original.to_string();
663        let parsed: StringM<10> = displayed.parse().unwrap();
664        assert_eq!(original, parsed);
665    }
666
667    #[test]
668    #[cfg(all(feature = "serde", feature = "alloc"))]
669    fn test_stringm_serde_serialize_rejects_overlimit() {
670        let value = StringM::<1>(b"ab".to_vec());
671        let err = serde_json::to_value(&value).unwrap_err();
672        let msg = alloc::format!("{err}");
673        assert!(msg.contains("max length exceeded"), "{msg}");
674    }
675
676    #[test]
677    #[cfg(all(feature = "serde", feature = "alloc"))]
678    fn test_stringm_serde_serialize_accepts_at_max() {
679        let value = StringM::<2>(b"ab".to_vec());
680        let json = serde_json::to_value(&value).unwrap();
681        assert_eq!(json, serde_json::json!("ab"));
682    }
683
684    #[test]
685    #[cfg(all(feature = "serde", feature = "alloc"))]
686    fn test_stringm_serde_roundtrip() {
687        // Non-UTF8 bytes round-trip via the `\xNN` escape encoding.
688        let value = StringM::<10>(vec![0xc3, 0xa9, b'a']);
689        let json = serde_json::to_value(&value).unwrap();
690        let back: StringM<10> = serde_json::from_value(json).unwrap();
691        assert_eq!(value, back);
692    }
693
694    #[test]
695    #[cfg(all(feature = "serde", feature = "alloc"))]
696    fn test_stringm_serde_deserialize_rejects_overlimit() {
697        let err = serde_json::from_value::<StringM<1>>(serde_json::json!("abc"));
698        assert!(err.is_err());
699    }
700}