Skip to main content

vpp_plugin/vlibapi/
mod.rs

1//! VPP API library
2//!
3//! Traits, types and helpers for working with API messages and client registrations.
4
5use std::{
6    borrow::Cow,
7    fmt,
8    hash::{Hash, Hasher},
9    marker::PhantomData,
10    mem::{self, MaybeUninit},
11    ops::{Deref, DerefMut},
12    ptr::{self, NonNull},
13    slice,
14    str::Utf8Error,
15};
16
17use crate::{
18    bindings::{
19        vl_api_helper_client_index_to_registration, vl_api_helper_send_msg, vl_api_registration_t,
20        vl_msg_api_alloc, vl_msg_api_free,
21    },
22    vlib::BarrierHeldMainRef,
23};
24
25pub mod num_unaligned;
26
27/// An owned VPP message buffer containing a `T`.
28///
29/// The message can be sent to a client using [`Registration::send_message`].
30///
31/// Important invariant:
32///
33/// - `T` must have an alignment of 1 (e.g. by `#[repr(packed)]`)
34pub struct Message<T: ?Sized> {
35    pointer: NonNull<T>,
36}
37
38impl<T> Message<T> {
39    /// Allocate a VPP message and initialise it by copying `value` into the
40    /// newly-allocated buffer.
41    ///
42    /// # Panics
43    ///
44    /// Panics if `align_of::<T>() != 1` because the VPP API message allocator does not provide
45    /// alignment guarantees; generated message structs are expected to be packed.
46    pub fn new(value: T) -> Self {
47        if std::mem::align_of::<T>() != 1 {
48            // It's unclear what alignment guarantees vpp gives. Possibly the memory is aligned
49            // to align_of(msghdr_t), but play it safe and required packed types -
50            // vpp-plugin-api-gen generated types always have this anyway.
51            panic!("Messages must only contain #[repr(packed)] types");
52        }
53
54        // SAFETY: `vl_msg_api_alloc` returns a pointer to at least `size_of::<T>()` bytes (or
55        // null on allocation failure). We have asserted `align_of::<T>() == 1` so the cast to
56        // `*mut T` is valid. It is safe to use `NonNull::new_unchecked` because the VPP
57        // allocation cannot fail and instead aborts on allocation failures.
58        unsafe {
59            let mut me = Self {
60                pointer: NonNull::new_unchecked(
61                    vl_msg_api_alloc(std::mem::size_of::<T>() as i32) as *mut T
62                ),
63            };
64            ptr::copy_nonoverlapping(&value, me.pointer.as_mut(), 1);
65            me
66        }
67    }
68
69    /// Allocate an uninitialised VPP message buffer for `T`.
70    ///
71    /// This returns a `Message<MaybeUninit<T>>`. Use [`Self::write`] or [`Self::assume_init`]
72    /// after manually initialising the contents.
73    ///
74    ///
75    /// # Panics
76    ///
77    /// Panics if `align_of::<T>() != 1` for the same reason as `new`.
78    pub fn new_uninit() -> Message<MaybeUninit<T>> {
79        if std::mem::align_of::<T>() != 1 {
80            // It's unclear what alignment guarantees vpp gives. Possibly the memory is aligned
81            // to align_of(msghdr_t), but play it safe and required packed types -
82            // vpp-plugin-api-gen generated types always have this anyway.
83            panic!("Messages must only contain #[repr(packed)] types");
84        }
85
86        // SAFETY: `vl_msg_api_alloc` returns a pointer to at least `size_of::<MaybeUninit<T>>()`
87        // bytes. Casting that pointer to `*mut MaybeUninit<T>` is valid as the buffer is
88        // uninitialised but suitably sized. It is safe to use `NonNull::new_unchecked` because
89        // the VPP allocation cannot fail and instead aborts on allocation failures.
90        unsafe {
91            Message {
92                pointer: NonNull::new_unchecked(vl_msg_api_alloc(
93                    std::mem::size_of::<MaybeUninit<T>>() as i32,
94                ) as *mut MaybeUninit<T>),
95            }
96        }
97    }
98}
99
100impl Message<u8> {
101    /// Allocate a VPP message buffer `nbytes` of `u8`s initialised to 0.
102    pub fn new_bytes(nbytes: u32) -> Message<u8> {
103        // SAFETY: `vl_msg_api_alloc` returns a pointer to at least `size_of::<MaybeUninit<T>>()`
104        // bytes. Casting that pointer to `*mut MaybeUninit<T>` is valid as the buffer is
105        // uninitialised but suitably sized. It is safe to use `NonNull::new_unchecked` because
106        // the VPP allocation cannot fail and instead aborts on allocation failures.
107        unsafe {
108            let mut me = Message {
109                pointer: NonNull::new_unchecked(vl_msg_api_alloc(nbytes as i32) as *mut u8),
110            };
111            ptr::write_bytes(me.pointer.as_mut(), 0, nbytes as usize);
112            me
113        }
114    }
115}
116
117impl<T: ?Sized> Message<T> {
118    /// Consume the `Message` and return the raw pointer to the underlying buffer
119    ///
120    /// The returned pointer becomes the caller's responsibility. The `Message` destructor will
121    /// not run for `m` and the underlying buffer will not be freed by Rust; callers must ensure
122    /// the buffer is eventually freed (for example by passing it to VPP or calling
123    /// `vl_msg_api_free`).
124    ///
125    /// Not a method on `Message` to avoid clashing with application methods of the same name on
126    /// the underlying type.
127    pub fn into_raw(m: Self) -> *mut T {
128        let m = mem::ManuallyDrop::new(m);
129        m.pointer.as_ptr()
130    }
131}
132
133impl<T> Message<MaybeUninit<T>> {
134    /// Convert a `Message<MaybeUninit<T>>` into a `Message<T>` without performing any
135    /// initialisation checks
136    ///
137    /// # Safety
138    ///
139    /// The caller must ensure that the underlying buffer is fully initialised for `T`. If the
140    /// memory is not properly initialised, using the resulting `Message<T>` is undefined
141    /// behaviour.
142    pub unsafe fn assume_init(self) -> Message<T> {
143        // SAFETY: The safety requirements are documented in the function's safety comment.
144        unsafe {
145            let pointer = Message::into_raw(self);
146            Message {
147                pointer: NonNull::new_unchecked(pointer as *mut T),
148            }
149        }
150    }
151
152    /// Initialise the previously-uninitialised buffer with `value` and return the initialised
153    /// `Message<T>`
154    pub fn write(mut self, value: T) -> Message<T> {
155        // SAFETY: We have exclusive ownership of the allocated buffer for
156        // `self`. Writing `value` into the `MaybeUninit<T>` buffer
157        // initialises it, after which `assume_init` converts the message to
158        // `Message<T>`.
159        unsafe {
160            (*self).write(value);
161            self.assume_init()
162        }
163    }
164}
165
166impl<T: ?Sized> Deref for Message<T> {
167    type Target = T;
168
169    fn deref(&self) -> &Self::Target {
170        // SAFETY: `self.pointer` was allocated by `vl_msg_api_alloc` and points to a valid,
171        // initialised `T` for the lifetime of `&self`.
172        unsafe { self.pointer.as_ref() }
173    }
174}
175
176impl<T: ?Sized> DerefMut for Message<T> {
177    fn deref_mut(&mut self) -> &mut Self::Target {
178        // SAFETY: `self.pointer` was allocated by `vl_msg_api_alloc` and we hold exclusive access
179        // via `&mut self`, so returning a mutable reference to the inner `T` is valid.
180        unsafe { self.pointer.as_mut() }
181    }
182}
183
184impl<T: Default> Default for Message<T> {
185    fn default() -> Self {
186        Self::new_uninit().write(Default::default())
187    }
188}
189
190impl<T: ?Sized> Drop for Message<T> {
191    fn drop(&mut self) {
192        // SAFETY: We own the underlying buffer and the memory is considered initialised for `T`
193        // at time of drop. It's therefore safe to drop the contained `T` and free the buffer
194        // with the VPP message API free function.
195        unsafe {
196            ptr::drop_in_place(self.pointer.as_ptr());
197            vl_msg_api_free(self.pointer.as_ptr().cast());
198        }
199    }
200}
201
202impl<T> From<T> for Message<T> {
203    fn from(value: T) -> Self {
204        Self::new(value)
205    }
206}
207
208impl<T: ?Sized + PartialOrd> PartialOrd for Message<T> {
209    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
210        (**self).partial_cmp(&**other)
211    }
212}
213
214impl<T: ?Sized + Ord> Ord for Message<T> {
215    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
216        (**self).cmp(&**other)
217    }
218}
219
220impl<T: ?Sized + PartialEq> PartialEq for Message<T> {
221    fn eq(&self, other: &Self) -> bool {
222        **self == **other
223    }
224}
225
226impl<T: ?Sized + Eq> Eq for Message<T> {}
227
228impl<T: ?Sized + Hash> Hash for Message<T> {
229    fn hash<H: Hasher>(&self, state: &mut H) {
230        (**self).hash(state);
231    }
232}
233
234impl<T: fmt::Display + ?Sized> fmt::Display for Message<T> {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        fmt::Display::fmt(&**self, f)
237    }
238}
239
240impl<T: fmt::Debug + ?Sized> fmt::Debug for Message<T> {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        fmt::Debug::fmt(&**self, f)
243    }
244}
245
246impl<T: ?Sized> fmt::Pointer for Message<T> {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        let ptr: *const T = &**self;
249        fmt::Pointer::fmt(&ptr, f)
250    }
251}
252
253/// Trait used by generated message types that require endian conversions.
254///
255/// Implementations should swap fields between host and network byte order.
256pub trait EndianSwap {
257    /// Swap the endianness of the message in-place.
258    ///
259    /// `to_net == true` indicates conversion from host to network order.
260    ///
261    /// # Safety
262    ///
263    /// The caller must ensure that if `self` contains a variable length array that elements
264    /// indexed from 0 up to the contents of the length field are initialised and contained within
265    /// the memory allocated for the object.
266    unsafe fn endian_swap(&mut self, to_net: bool);
267}
268
269/// Registration state for the VPP side of an API client
270///
271/// A `&mut Registration` corresponds to a C `vl_api_registration *`.
272///
273/// Use [`RegistrationScope::from_client_index`] to obtain a mutable reference.
274#[repr(transparent)]
275pub struct Registration(foreign_types::Opaque);
276
277impl Registration {
278    /// Construct a `&mut Registration` from a raw `vl_api_registration_t` pointer.
279    ///
280    /// # Safety
281    ///
282    /// - `ptr` must be a valid, non-null pointer to a `vl_api_registration_t`.
283    /// - The caller must ensure exclusive mutable access for the returned lifetime `'a` (no other
284    ///   references or concurrent uses may alias the same underlying registration for the
285    ///   duration of the returned borrow).
286    /// - The pointer must remain valid for the returned lifetime and must not be freed or
287    ///   invalidated while the borrow is active.
288    pub unsafe fn from_ptr_mut<'a>(ptr: *mut vl_api_registration_t) -> &'a mut Self {
289        // SAFETY: The safety requirements are documented in the function's safety comment.
290        unsafe { &mut *(ptr as *mut _) }
291    }
292
293    /// Return the raw `vl_api_registration_t` pointer for this `Registration`.
294    pub fn as_ptr(&self) -> *mut vl_api_registration_t {
295        self as *const _ as *mut _
296    }
297
298    /// Send a message to the registration.
299    ///
300    /// This consumes `message` and transfers ownership of the underlying buffer to VPP.
301    pub fn send_message<T>(&mut self, message: Message<T>) {
302        // SAFETY: `self.as_ptr()` returns a raw `vl_api_registration_t` pointer that is valid
303        // for the duration of this call. `Message::into_raw` transfers ownership of the message
304        // buffer and yields a pointer that is safe to pass to the C API; the C API takes
305        // ownership of the buffer. `vl_api_helper_send_msg` is called with valid pointers.
306        unsafe {
307            vl_api_helper_send_msg(self.as_ptr(), Message::into_raw(message).cast());
308        }
309    }
310}
311
312/// Scope helper used to obtain short-lived `&mut Registration` borrows.
313///
314/// This enforces that `Registration` references obtained cannot be retained beyond the
315/// `registration_scope` function call
316pub struct RegistrationScope<'scope>(PhantomData<&'scope ()>);
317
318impl<'scope> RegistrationScope<'scope> {
319    /// Look up a `Registration` by VPP client index.
320    ///
321    /// Returns `Some(&mut Registration)` when the client index corresponds to a current
322    /// registration, or `None` if no registration exists for that index.
323    pub fn from_client_index(
324        &self,
325        _vm: &BarrierHeldMainRef,
326        client_index: u32,
327    ) -> Option<&'scope mut Registration> {
328        // SAFETY: `vl_api_helper_client_index_to_registration` returns either a null pointer or
329        // a valid pointer to a `vl_api_registration_t` that lives as long as the corresponding
330        // client registration in VPP. The lifetime of the returned reference ensures the caller
331        // cannot retain that reference beyond the intended scope.
332        unsafe {
333            let ptr = vl_api_helper_client_index_to_registration(client_index.to_be());
334            if ptr.is_null() {
335                None
336            } else {
337                Some(Registration::from_ptr_mut(ptr))
338            }
339        }
340    }
341}
342
343/// Execute a closure with a temporary `RegistrationScope`.
344///
345/// Used to ensure any `&mut Registration` borrows that are obtained are tied to the lifetime of
346/// the closure and cannot accidentally escape.
347pub fn registration_scope<F, T>(f: F) -> T
348where
349    F: for<'scope> FnOnce(&'scope RegistrationScope<'scope>) -> T,
350{
351    let scope = RegistrationScope(PhantomData);
352    f(&scope)
353}
354
355/// A stream for sending messages to a registration
356pub struct Stream<'scope, T> {
357    registration: &'scope mut Registration,
358    _phantom: PhantomData<T>,
359}
360
361impl<'scope, T> Stream<'scope, T> {
362    /// Creates a new stream from a mutable reference to a registration.
363    pub fn new(registration: &'scope mut Registration) -> Self {
364        Self {
365            registration,
366            _phantom: PhantomData,
367        }
368    }
369
370    /// Sends a network-endian (big-endian) message to the registration
371    ///
372    /// Since the message is in network order, then it is sent without performing an endian swap.
373    pub fn send_message_ne(&mut self, message: Message<T>) {
374        self.registration.send_message(message);
375    }
376
377    /// Consumes the stream and returns the underlying registration reference.
378    pub fn into_inner(self) -> &'scope mut Registration {
379        self.registration
380    }
381}
382
383impl<'scope, T: EndianSwap> Stream<'scope, T> {
384    /// Sends a message to the registration after performing endian swap to network order.
385    ///
386    /// # Safety
387    ///
388    /// The caller must ensure that if `messages` contains a variable length array that elements
389    /// indexed from 0 up to the contents of the length field are initialised and contained within
390    /// the memory allocated for the object.
391    pub unsafe fn send_message(&mut self, mut message: Message<T>) {
392        // SAFETY: The safety requirements are documented in the function's safety comment.
393        unsafe {
394            message.endian_swap(true);
395            self.send_message_ne(message);
396        }
397    }
398}
399
400#[repr(C, packed)]
401#[derive(Copy, Clone, Default)]
402/// A string type used in VPP API messages.
403///
404/// This represents a variable-length string with a length prefix,
405/// commonly used in VPP API message structures.
406///
407/// Note that copying/cloning `ApiString` objects will not copy/clone the contents of the string.
408pub struct ApiString {
409    length: u32,
410    buf: [u8; 0],
411}
412
413impl ApiString {
414    /// Returns the length of the string in bytes.
415    pub const fn len(&self) -> u32 {
416        self.length
417    }
418
419    /// Returns `true` if the string has a length of zero.
420    pub const fn is_empty(&self) -> bool {
421        self.length == 0
422    }
423
424    /// Returns a byte slice of the string's contents.
425    pub fn as_bytes(&self) -> &[u8] {
426        // SAFETY: The buffer memory is valid and initialised for at least self.length bytes.
427        unsafe {
428            slice::from_raw_parts(
429                std::ptr::addr_of!(self.buf) as *const u8,
430                self.length as usize,
431            )
432        }
433    }
434
435    /// Returns a mutable byte slice of the string's contents.
436    fn as_bytes_mut(&mut self) -> &mut [u8] {
437        // SAFETY: The buffer memory is valid and initialised for at least self.length bytes.
438        unsafe {
439            slice::from_raw_parts_mut(
440                std::ptr::addr_of_mut!(self.buf) as *mut u8,
441                self.length as usize,
442            )
443        }
444    }
445
446    /// Converts the string to a `&str` slice.
447    ///
448    /// If the contents of the `ApiString` are valid UTF-8 data, this
449    /// function will return the corresponding `&[str]` slice. Otherwise,
450    /// it will return an error with details of where UTF-8 validation failed.
451    pub fn to_str(&self) -> Result<&str, Utf8Error> {
452        str::from_utf8(self.as_bytes())
453    }
454
455    /// Converts the string to a `Cow<str>`, replacing invalid UTF-8 sequences with �.
456    pub fn to_string_lossy(&self) -> Cow<'_, str> {
457        String::from_utf8_lossy(self.as_bytes())
458    }
459
460    /// Sets the length of the string in bytes.
461    ///
462    /// # Safety
463    ///
464    /// The caller must ensure that the underlying buffer has at least `length` bytes of valid
465    /// memory and is initialised.
466    pub unsafe fn set_len(&mut self, length: u32) {
467        self.length = length;
468    }
469
470    /// Copies the contents of the given string into this `ApiString`.
471    ///
472    /// # Panics
473    ///
474    /// Panics if the length of the `ApiString` is different to the length of the string in bytes.
475    pub fn copy_from_str(&mut self, s: &str) {
476        self.as_bytes_mut().copy_from_slice(s.as_bytes());
477    }
478}
479
480impl std::fmt::Debug for ApiString {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        std::fmt::Debug::fmt(&self.to_string_lossy(), f)
483    }
484}
485
486impl EndianSwap for ApiString {
487    unsafe fn endian_swap(&mut self, _to_net: bool) {
488        self.length = self.length.to_be();
489        // No endian swap necessary for self.buf
490    }
491}
492
493#[repr(C)]
494#[derive(Copy, Clone, PartialEq, Eq)]
495/// A string type used in VPP API messages.
496///
497/// This represents a fixed-length, nul-terminated string,
498/// commonly used in VPP API message structures.
499pub struct ApiFixedString<const N: usize> {
500    buf: [u8; N],
501}
502
503impl<const N: usize> ApiFixedString<N> {
504    /// Returns the length of the string in bytes.
505    pub const fn len(&self) -> usize {
506        let mut len = 0;
507        while self.buf[len] != 0 {
508            len += 1;
509        }
510        len
511    }
512
513    /// Returns `true` if the string has a length of zero.
514    pub const fn is_empty(&self) -> bool {
515        self.len() == 0
516    }
517
518    /// Returns a byte slice of the string's contents not including the nul-terminator.
519    pub fn as_bytes(&self) -> &[u8] {
520        &self.buf[..self.len()]
521    }
522
523    /// Converts the string to a `&str` slice.
524    ///
525    /// If the contents of the `ApiFixedString` are valid UTF-8 data, this
526    /// function will return the corresponding `&[str]` slice. Otherwise,
527    /// it will return an error with details of where UTF-8 validation failed.
528    pub fn to_str(&self) -> Result<&str, Utf8Error> {
529        str::from_utf8(self.as_bytes())
530    }
531
532    /// Converts the string to a `Cow<str>`, replacing invalid UTF-8 sequences with �.
533    pub fn to_string_lossy(&self) -> Cow<'_, str> {
534        String::from_utf8_lossy(self.as_bytes())
535    }
536
537    /// Copies the contents of the given string into this `ApiFixedString`.
538    ///
539    /// # Panics
540    ///
541    /// Panics if the string exceeds the capacity of the fixed buffer.
542    pub fn copy_from_str(&mut self, s: &str) {
543        let bytes = s.as_bytes();
544        self.buf[0..bytes.len()].copy_from_slice(bytes);
545        // Set any remaining elements to 0 since they are serialised to the wire and so we don't
546        // want any stale data, plus the first 0 acts as a nul-terminator.
547        if bytes.len() + 1 < self.buf.len() {
548            self.buf[bytes.len()..].fill(0);
549        }
550    }
551}
552
553impl<const N: usize> std::fmt::Debug for ApiFixedString<N> {
554    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555        std::fmt::Debug::fmt(&self.to_string_lossy(), f)
556    }
557}
558
559impl<const N: usize> EndianSwap for ApiFixedString<N> {
560    unsafe fn endian_swap(&mut self, _to_net: bool) {
561        // No endian swap necessary for self.buf
562    }
563}
564
565impl<const N: usize> Default for ApiFixedString<N> {
566    fn default() -> Self {
567        Self { buf: [0; N] }
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use std::borrow::Cow;
575
576    #[test]
577    fn test_fixed_string_default() {
578        let s: ApiFixedString<10> = Default::default();
579        assert_eq!(s.len(), 0);
580        assert!(s.is_empty());
581        assert_eq!(s.as_bytes(), &[]);
582        assert_eq!(s.to_string_lossy(), Cow::Borrowed(""));
583    }
584
585    #[test]
586    fn test_fixed_string_copy_from_str() {
587        let mut s: ApiFixedString<10> = Default::default();
588        s.copy_from_str("hello");
589        assert_eq!(s.len(), 5);
590        assert!(!s.is_empty());
591        assert_eq!(s.as_bytes(), b"hello");
592        assert_eq!(s.to_string_lossy(), Cow::Borrowed("hello"));
593    }
594
595    #[test]
596    fn test_fixed_string_copy_from_str_with_padding() {
597        let mut s: ApiFixedString<10> = Default::default();
598        s.copy_from_str("hi");
599        assert_eq!(s.len(), 2);
600        assert_eq!(s.as_bytes(), b"hi");
601        // Check that the rest is zeroed
602        assert_eq!(s.buf[2..], [0; 8]);
603    }
604
605    #[test]
606    fn test_fixed_string_copy_from_str_empty() {
607        let mut s: ApiFixedString<10> = Default::default();
608        s.copy_from_str("");
609        assert_eq!(s.len(), 0);
610        assert!(s.is_empty());
611    }
612
613    #[test]
614    fn test_fixed_string_copy_from_str_max_length() {
615        let mut s: ApiFixedString<5> = Default::default();
616        s.copy_from_str("abcd"); // 4 chars, should fit with nul
617        assert_eq!(s.len(), 4);
618        assert_eq!(s.as_bytes(), b"abcd");
619    }
620
621    #[test]
622    #[should_panic]
623    fn test_fixed_string_copy_from_str_too_long() {
624        let mut s: ApiFixedString<5> = Default::default();
625        s.copy_from_str("abcdef"); // 6 chars, too long
626    }
627
628    #[test]
629    fn test_fixed_string_to_str_valid_utf8() {
630        let mut s: ApiFixedString<10> = Default::default();
631        s.copy_from_str("hello");
632        assert_eq!(s.to_str().unwrap(), "hello");
633    }
634
635    #[test]
636    fn test_fixed_string_to_str_invalid_utf8() {
637        let mut s: ApiFixedString<10> = Default::default();
638        // Manually set invalid UTF-8
639        s.buf[0] = 0xff;
640        s.buf[1] = 0xfe;
641        s.buf[2] = 0;
642        assert!(s.to_str().is_err());
643    }
644
645    #[test]
646    fn test_fixed_string_to_string_lossy_invalid_utf8() {
647        let mut s: ApiFixedString<10> = Default::default();
648        s.copy_from_str("hello");
649        assert_eq!(s.to_string_lossy(), "hello");
650
651        // Invalid UTF-8
652        s.buf[0] = 0xff;
653        s.buf[1] = 0;
654        assert_eq!(s.to_string_lossy(), "�");
655    }
656
657    #[test]
658    fn test_fixed_string_debug() {
659        let mut s: ApiFixedString<10> = Default::default();
660        s.copy_from_str("test");
661        assert_eq!(format!("{:?}", s), "\"test\"");
662    }
663
664    #[test]
665    fn test_fixed_string_partialeq() {
666        // Fill part of the rest of the string to ensure it has no effect when a shorter string is
667        // copied over
668        let mut s1: ApiFixedString<10> = Default::default();
669        s1.copy_from_str("test");
670        assert_eq!(s1.to_string_lossy(), "test");
671
672        s1.copy_from_str("te");
673
674        let mut s2: ApiFixedString<10> = Default::default();
675        s2.copy_from_str("te");
676
677        assert_eq!(s1, s2);
678    }
679}