mqtt_protocol_core/mqtt/common/
arc_payload.rs

1// MIT License
2//
3// Copyright (c) 2025 Takatoshi Kondo
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23use crate::mqtt::Arc;
24use alloc::{string::String, vec::Vec};
25use serde::{Serialize, Serializer};
26
27// SSO buffer size configuration - priority-based selection for maximum size
28#[cfg(feature = "sso-lv20")]
29const SSO_BUFFER_SIZE: usize = 255; // Highest priority: 255 bytes
30#[cfg(all(not(feature = "sso-lv20"), feature = "sso-lv10"))]
31const SSO_BUFFER_SIZE: usize = 127; // Second priority: 127 bytes
32#[cfg(all(
33    not(any(feature = "sso-lv20", feature = "sso-lv10")),
34    feature = "sso-min-64bit"
35))]
36const SSO_BUFFER_SIZE: usize = 31; // Third priority: 31 bytes
37#[cfg(all(
38    not(any(feature = "sso-lv20", feature = "sso-lv10", feature = "sso-min-64bit")),
39    feature = "sso-min-32bit"
40))]
41const SSO_BUFFER_SIZE: usize = 15; // Fourth priority: 15 bytes
42#[cfg(not(any(
43    feature = "sso-min-32bit",
44    feature = "sso-min-64bit",
45    feature = "sso-lv10",
46    feature = "sso-lv20"
47)))]
48#[allow(dead_code)]
49const SSO_BUFFER_SIZE: usize = 0; // No SSO features enabled
50
51// Length type is always u8 since all buffer sizes fit in u8 range (max 255)
52#[cfg(any(
53    feature = "sso-min-32bit",
54    feature = "sso-min-64bit",
55    feature = "sso-lv10",
56    feature = "sso-lv20"
57))]
58type LengthType = u8;
59
60#[cfg(not(any(
61    feature = "sso-min-32bit",
62    feature = "sso-min-64bit",
63    feature = "sso-lv10",
64    feature = "sso-lv20"
65)))]
66#[allow(dead_code)]
67type LengthType = u8;
68
69/// A reference-counted byte payload with slice semantics
70///
71/// `ArcPayload` provides an efficient way to handle byte data by using `Arc<[u8]>`
72/// for reference counting, combined with offset and length information to represent
73/// a slice view of the underlying data. This allows for zero-copy sharing of payload
74/// data across multiple consumers while maintaining slice-like semantics.
75#[derive(Clone)]
76#[allow(clippy::large_enum_variant)]
77pub enum ArcPayload {
78    #[cfg(any(
79        feature = "sso-min-32bit",
80        feature = "sso-min-64bit",
81        feature = "sso-lv10",
82        feature = "sso-lv20"
83    ))]
84    Small([u8; SSO_BUFFER_SIZE], LengthType), // buffer, actual_length
85    Large {
86        data: Arc<[u8]>,
87        start: usize,
88        length: usize,
89    },
90}
91
92impl PartialEq for ArcPayload {
93    fn eq(&self, other: &Self) -> bool {
94        self.as_slice() == other.as_slice()
95    }
96}
97
98impl Eq for ArcPayload {}
99
100impl Serialize for ArcPayload {
101    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102    where
103        S: Serializer,
104    {
105        self.as_slice().serialize(serializer)
106    }
107}
108
109impl ArcPayload {
110    /// Create a new `ArcPayload` from reference-counted data with specified range
111    ///
112    /// Creates a new payload that represents a slice view of the provided `Arc<[u8]>` data
113    /// starting at the specified offset with the given length.
114    ///
115    /// # Parameters
116    ///
117    /// * `data` - The reference-counted byte data
118    /// * `start` - The starting offset within the data
119    /// * `length` - The length of the payload slice
120    ///
121    /// # Panics
122    ///
123    /// Panics in debug mode if `start + length > data.len()` (payload out of bounds)
124    ///
125    /// # Examples
126    ///
127    /// ```ignore
128    /// use alloc::sync::Arc;
129    /// use mqtt_protocol_core::mqtt::ArcPayload;
130    ///
131    /// let data = Arc::from(&b"hello world"[..]);
132    /// let payload = ArcPayload::new(data, 0, 5); // "hello"
133    /// ```
134    pub fn new(data: Arc<[u8]>, start: usize, length: usize) -> Self {
135        debug_assert!(start + length <= data.len(), "payload out of bounds",);
136
137        #[cfg(any(
138            feature = "sso-min-32bit",
139            feature = "sso-min-64bit",
140            feature = "sso-lv10",
141            feature = "sso-lv20"
142        ))]
143        if length <= SSO_BUFFER_SIZE {
144            let mut buffer = [0u8; SSO_BUFFER_SIZE];
145            buffer[..length].copy_from_slice(&data[start..start + length]);
146            return Self::Small(buffer, length as LengthType);
147        }
148
149        Self::Large {
150            data,
151            start,
152            length,
153        }
154    }
155
156    /// Get a slice view of the payload data
157    ///
158    /// Returns a byte slice representing the payload data within the specified range.
159    ///
160    /// # Returns
161    ///
162    /// A `&[u8]` slice of the payload data
163    pub fn as_slice(&self) -> &[u8] {
164        match self {
165            ArcPayload::Large {
166                data,
167                start,
168                length,
169            } => &data[*start..*start + *length],
170            #[cfg(any(
171                feature = "sso-min-32bit",
172                feature = "sso-min-64bit",
173                feature = "sso-lv10",
174                feature = "sso-lv20"
175            ))]
176            ArcPayload::Small(buffer, length) => &buffer[..*length as usize],
177        }
178    }
179
180    /// Get the length of the payload
181    ///
182    /// Returns the number of bytes in the payload slice.
183    ///
184    /// # Returns
185    ///
186    /// The length of the payload in bytes
187    pub fn len(&self) -> usize {
188        match self {
189            ArcPayload::Large { length, .. } => *length,
190            #[cfg(any(
191                feature = "sso-min-32bit",
192                feature = "sso-min-64bit",
193                feature = "sso-lv10",
194                feature = "sso-lv20"
195            ))]
196            ArcPayload::Small(_, length) => *length as usize,
197        }
198    }
199
200    /// Check if the payload is empty
201    ///
202    /// Returns `true` if the payload contains no bytes.
203    ///
204    /// # Returns
205    ///
206    /// `true` if the payload length is zero, `false` otherwise
207    pub fn is_empty(&self) -> bool {
208        self.len() == 0
209    }
210
211    /// Get a reference to the underlying `Arc<[u8]>` data
212    ///
213    /// Returns a reference to the reference-counted byte array that contains
214    /// the actual data. This provides access to the full underlying data,
215    /// not just the slice view represented by this payload.
216    ///
217    /// # Returns
218    ///
219    /// A reference to the underlying `Arc<[u8]>` data
220    pub fn arc_data(&self) -> Option<&Arc<[u8]>> {
221        match self {
222            ArcPayload::Large { data, .. } => Some(data),
223            #[cfg(any(
224                feature = "sso-min-32bit",
225                feature = "sso-min-64bit",
226                feature = "sso-lv10",
227                feature = "sso-lv20"
228            ))]
229            ArcPayload::Small(_, _) => None, // Small variant doesn't use Arc data
230        }
231    }
232}
233
234impl Default for ArcPayload {
235    fn default() -> Self {
236        #[cfg(any(
237            feature = "sso-min-32bit",
238            feature = "sso-min-64bit",
239            feature = "sso-lv10",
240            feature = "sso-lv20"
241        ))]
242        return ArcPayload::Small([0u8; SSO_BUFFER_SIZE], 0 as LengthType);
243
244        #[cfg(not(any(
245            feature = "sso-min-32bit",
246            feature = "sso-min-64bit",
247            feature = "sso-lv10",
248            feature = "sso-lv20"
249        )))]
250        return ArcPayload::Large {
251            data: Arc::from(&[] as &[u8]),
252            start: 0,
253            length: 0,
254        };
255    }
256}
257
258impl core::fmt::Debug for ArcPayload {
259    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
260        f.debug_struct("ArcPayload")
261            .field("data", &self.as_slice())
262            .field("len", &self.len())
263            .finish()
264    }
265}
266
267/// Trait for converting various types into `ArcPayload`
268///
269/// This trait provides a uniform interface for converting different data types
270/// into `ArcPayload` instances. It allows for convenient creation of payloads
271/// from common types like strings, byte slices, vectors, and arrays.
272pub trait IntoPayload {
273    /// Convert the value into an `ArcPayload`
274    ///
275    /// # Returns
276    ///
277    /// An `ArcPayload` containing the converted data
278    fn into_payload(self) -> ArcPayload;
279}
280
281/// Convert a string slice (`&str`) into an `ArcPayload`
282impl IntoPayload for &str {
283    fn into_payload(self) -> ArcPayload {
284        let bytes = self.as_bytes();
285
286        #[cfg(any(
287            feature = "sso-min-32bit",
288            feature = "sso-min-64bit",
289            feature = "sso-lv10",
290            feature = "sso-lv20"
291        ))]
292        if bytes.len() <= SSO_BUFFER_SIZE {
293            let mut buffer = [0u8; SSO_BUFFER_SIZE];
294            buffer[..bytes.len()].copy_from_slice(bytes);
295            return ArcPayload::Small(buffer, bytes.len() as LengthType);
296        }
297
298        ArcPayload::Large {
299            data: Arc::from(bytes),
300            start: 0,
301            length: bytes.len(),
302        }
303    }
304}
305
306/// Convert an owned string (`String`) into an `ArcPayload`
307impl IntoPayload for String {
308    fn into_payload(self) -> ArcPayload {
309        let bytes = self.as_bytes();
310
311        #[cfg(any(
312            feature = "sso-min-32bit",
313            feature = "sso-min-64bit",
314            feature = "sso-lv10",
315            feature = "sso-lv20"
316        ))]
317        if bytes.len() <= SSO_BUFFER_SIZE {
318            let mut buffer = [0u8; SSO_BUFFER_SIZE];
319            buffer[..bytes.len()].copy_from_slice(bytes);
320            return ArcPayload::Small(buffer, bytes.len() as LengthType);
321        }
322
323        ArcPayload::Large {
324            data: Arc::from(bytes),
325            start: 0,
326            length: bytes.len(),
327        }
328    }
329}
330
331/// Convert a byte slice (`&[u8]`) into an `ArcPayload`
332impl IntoPayload for &[u8] {
333    fn into_payload(self) -> ArcPayload {
334        #[cfg(any(
335            feature = "sso-min-32bit",
336            feature = "sso-min-64bit",
337            feature = "sso-lv10",
338            feature = "sso-lv20"
339        ))]
340        if self.len() <= SSO_BUFFER_SIZE {
341            let mut buffer = [0u8; SSO_BUFFER_SIZE];
342            buffer[..self.len()].copy_from_slice(self);
343            return ArcPayload::Small(buffer, self.len() as LengthType);
344        }
345
346        ArcPayload::Large {
347            data: Arc::from(self),
348            start: 0,
349            length: self.len(),
350        }
351    }
352}
353
354/// Convert an owned byte vector (`Vec<u8>`) into an `ArcPayload`
355impl IntoPayload for Vec<u8> {
356    fn into_payload(self) -> ArcPayload {
357        #[cfg(any(
358            feature = "sso-min-32bit",
359            feature = "sso-min-64bit",
360            feature = "sso-lv10",
361            feature = "sso-lv20"
362        ))]
363        if self.len() <= SSO_BUFFER_SIZE {
364            let mut buffer = [0u8; SSO_BUFFER_SIZE];
365            buffer[..self.len()].copy_from_slice(&self);
366            return ArcPayload::Small(buffer, self.len() as LengthType);
367        }
368
369        let len = self.len();
370        ArcPayload::Large {
371            data: Arc::from(self),
372            start: 0,
373            length: len,
374        }
375    }
376}
377
378/// Convert a reference to a byte vector (`&Vec<u8>`) into an `ArcPayload`
379impl IntoPayload for &Vec<u8> {
380    fn into_payload(self) -> ArcPayload {
381        let slice: &[u8] = self.as_slice();
382
383        #[cfg(any(
384            feature = "sso-min-32bit",
385            feature = "sso-min-64bit",
386            feature = "sso-lv10",
387            feature = "sso-lv20"
388        ))]
389        if slice.len() <= SSO_BUFFER_SIZE {
390            let mut buffer = [0u8; SSO_BUFFER_SIZE];
391            buffer[..slice.len()].copy_from_slice(slice);
392            return ArcPayload::Small(buffer, slice.len() as LengthType);
393        }
394
395        ArcPayload::Large {
396            data: Arc::from(slice),
397            start: 0,
398            length: slice.len(),
399        }
400    }
401}
402
403/// Convert a reference to a byte array (`&[u8; N]`) into an `ArcPayload`
404impl<const N: usize> IntoPayload for &[u8; N] {
405    fn into_payload(self) -> ArcPayload {
406        let slice: &[u8] = self.as_slice();
407
408        #[cfg(any(
409            feature = "sso-min-32bit",
410            feature = "sso-min-64bit",
411            feature = "sso-lv10",
412            feature = "sso-lv20"
413        ))]
414        if slice.len() <= SSO_BUFFER_SIZE {
415            let mut buffer = [0u8; SSO_BUFFER_SIZE];
416            buffer[..slice.len()].copy_from_slice(slice);
417            return ArcPayload::Small(buffer, slice.len() as LengthType);
418        }
419
420        ArcPayload::Large {
421            data: Arc::from(slice),
422            start: 0,
423            length: slice.len(),
424        }
425    }
426}
427
428/// Convert an `Arc<[u8]>` directly into an `ArcPayload`
429impl IntoPayload for Arc<[u8]> {
430    fn into_payload(self) -> ArcPayload {
431        let len = self.len();
432
433        #[cfg(any(
434            feature = "sso-min-32bit",
435            feature = "sso-min-64bit",
436            feature = "sso-lv10",
437            feature = "sso-lv20"
438        ))]
439        if len <= SSO_BUFFER_SIZE {
440            let mut buffer = [0u8; SSO_BUFFER_SIZE];
441            buffer[..len].copy_from_slice(&self);
442            return ArcPayload::Small(buffer, len as LengthType);
443        }
444
445        ArcPayload::Large {
446            data: self,
447            start: 0,
448            length: len,
449        }
450    }
451}
452
453/// Convert unit type (`()`) into an empty `ArcPayload`
454impl IntoPayload for () {
455    fn into_payload(self) -> ArcPayload {
456        ArcPayload::default() // Empty payload
457    }
458}
459
460/// Identity conversion for `ArcPayload` (no-op)
461impl IntoPayload for ArcPayload {
462    fn into_payload(self) -> ArcPayload {
463        self
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_empty_payload() {
473        let payload = ArcPayload::default();
474        assert_eq!(payload.len(), 0);
475        assert!(payload.is_empty());
476        assert_eq!(payload.as_slice(), &[] as &[u8]);
477    }
478
479    #[test]
480    fn test_small_payload() {
481        let data = b"hello";
482        let payload = data.into_payload();
483        assert_eq!(payload.len(), 5);
484        assert!(!payload.is_empty());
485        assert_eq!(payload.as_slice(), b"hello");
486    }
487
488    #[test]
489    fn test_payload_variants() {
490        // Test small data (Small variant if any SSO feature is enabled)
491        let small_data = b"small";
492        let payload = small_data.into_payload();
493
494        #[cfg(any(
495            feature = "sso-min-32bit",
496            feature = "sso-min-64bit",
497            feature = "sso-lv10",
498            feature = "sso-lv20"
499        ))]
500        assert!(matches!(payload, ArcPayload::Small(_, _)));
501
502        #[cfg(not(any(
503            feature = "sso-min-32bit",
504            feature = "sso-min-64bit",
505            feature = "sso-lv10",
506            feature = "sso-lv20"
507        )))]
508        assert!(matches!(payload, ArcPayload::Large { .. }));
509
510        // Test data that should be Large if using smaller SSO buffers, but Small with sso-lv20
511        let medium_data = vec![0u8; 200]; // 200 bytes - larger than most SSO buffers but smaller than sso-lv20
512        let _payload = medium_data.into_payload();
513
514        // With sso-lv20 (255 bytes), this should be Small
515        #[cfg(feature = "sso-lv20")]
516        assert!(matches!(_payload, ArcPayload::Small(_, _)));
517
518        // Without sso-lv20, this should be Large (exceeds other SSO buffer sizes)
519        #[cfg(all(
520            any(
521                feature = "sso-min-32bit",
522                feature = "sso-min-64bit",
523                feature = "sso-lv10"
524            ),
525            not(feature = "sso-lv20")
526        ))]
527        assert!(matches!(_payload, ArcPayload::Large { .. }));
528
529        // Test data that should always be Large variant (larger than largest SSO buffer)
530        let very_large_data = b"This is a very long payload that exceeds even the largest SSO buffer size of 255 bytes. It should definitely be stored in the Large variant regardless of which SSO feature flags are enabled. This ensures consistent behavior across all configurations and provides a reliable test case.";
531        let payload = very_large_data.into_payload();
532        assert!(matches!(payload, ArcPayload::Large { .. }));
533    }
534
535    #[test]
536    fn test_arc_data_access() {
537        let small_data = b"test";
538        let small_payload = small_data.into_payload();
539
540        // Use data larger than largest SSO buffer to ensure Large variant
541        let very_large_data = b"This is a very long payload that exceeds even the largest SSO buffer size of 255 bytes. It should definitely be stored in the Large variant regardless of which SSO feature flags are enabled. This ensures consistent behavior across all configurations and provides a reliable test case for arc_data access.";
542        let large_payload = very_large_data.into_payload();
543
544        // Small variant should return None for arc_data when SSO is enabled
545        #[cfg(any(
546            feature = "sso-min-32bit",
547            feature = "sso-min-64bit",
548            feature = "sso-lv10",
549            feature = "sso-lv20"
550        ))]
551        if let ArcPayload::Small(_, _) = small_payload {
552            assert!(small_payload.arc_data().is_none());
553        }
554
555        // Without SSO, small data also uses Large variant
556        #[cfg(not(any(
557            feature = "sso-min-32bit",
558            feature = "sso-min-64bit",
559            feature = "sso-lv10",
560            feature = "sso-lv20"
561        )))]
562        assert!(small_payload.arc_data().is_some());
563
564        // Large variant should always return Some for arc_data
565        assert!(large_payload.arc_data().is_some());
566    }
567
568    #[test]
569    fn test_into_payload_implementations() {
570        // Test various types
571        let str_payload = "hello".into_payload();
572        assert_eq!(str_payload.as_slice(), b"hello");
573
574        let string_payload = String::from("world").into_payload();
575        assert_eq!(string_payload.as_slice(), b"world");
576
577        let vec_payload = vec![1, 2, 3, 4].into_payload();
578        assert_eq!(vec_payload.as_slice(), &[1, 2, 3, 4]);
579
580        let arr_payload = (&[5, 6, 7, 8]).into_payload();
581        assert_eq!(arr_payload.as_slice(), &[5, 6, 7, 8]);
582
583        let unit_payload = ().into_payload();
584        assert!(unit_payload.is_empty());
585    }
586}