Skip to main content

iceoryx2_bb_container/
semantic_string.rs

1// Copyright (c) 2023 Contributors to the Eclipse Foundation
2//
3// See the NOTICE file(s) distributed with this work for additional
4// information regarding copyright ownership.
5//
6// This program and the accompanying materials are made available under the
7// terms of the Apache Software License 2.0 which is available at
8// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license
9// which is available at https://opensource.org/licenses/MIT.
10//
11// SPDX-License-Identifier: Apache-2.0 OR MIT
12
13//! The [`SemanticString`](crate::semantic_string::SemanticString) is a trait for
14//! [`StaticString`](crate::string::StaticString) to create
15//! strong string types with semantic content contracts. They can be created
16//! with the help of the [`semantic_string`](crate::semantic_string!) macro.
17//!
18//! # Example, create a string that can contain a posix group name
19//!
20//! ```
21//! extern crate alloc;
22//!
23//! pub use iceoryx2_bb_container::semantic_string::SemanticString;
24//! use iceoryx2_bb_derive_macros::ZeroCopySend;
25//! use iceoryx2_bb_elementary_traits::zero_copy_send::ZeroCopySend;
26//!
27//! use core::hash::{Hash, Hasher};
28//! use iceoryx2_bb_container::semantic_string;
29//!
30//! const GROUP_NAME_LENGTH: usize = 31;
31//! semantic_string! {
32//!   // Name of the type
33//!   name: GroupName,
34//!   // The underlying capacity of the StaticString
35//!   capacity: GROUP_NAME_LENGTH,
36//!   // Callable that shall return true when the provided string contains invalid content
37//!   invalid_content: |string: &[u8]| {
38//!     if string.is_empty() {
39//!         // group names are not allowed to be empty
40//!         return true;
41//!     }
42//!
43//!     // group names are not allowed to start with a number or -
44//!     matches!(string[0], b'-' | b'0'..=b'9')
45//!   },
46//!   // Callable that shall return true when the provided string contains invalid characters
47//!   invalid_characters: |string: &[u8]| {
48//!     for value in string {
49//!         match value {
50//!             // only non-capital letters, numbers and - is allowed
51//!             b'a'..=b'z' | b'0'..=b'9' | b'-' => (),
52//!             _ => return true,
53//!         }
54//!     }
55//!
56//!     false
57//!   },
58//!   // When a SemanticString has multiple representations of the same semantic content, this
59//!   // callable shall convert the content to a uniform representation.
60//!   // Example: The path to `/tmp` can be also expressed as `/tmp/` or `////tmp////`
61//!   normalize: |this: &GroupName| {
62//!       this.clone()
63//!   }
64//! }
65//! ```
66
67use crate::string::*;
68use core::fmt::{Debug, Display};
69use core::hash::Hash;
70use core::ops::Deref;
71use iceoryx2_log::fail;
72
73/// Failures that can occur when a [`SemanticString`] is created or modified
74#[derive(Debug, Clone, Copy, Eq, PartialEq)]
75pub enum SemanticStringError {
76    /// The modification would lead to a [`SemanticString`] with invalid content.
77    InvalidContent,
78    /// The added content would exceed the maximum capacity of the [`SemanticString`]
79    ExceedsMaximumLength,
80}
81
82impl From<StringModificationError> for SemanticStringError {
83    fn from(value: StringModificationError) -> Self {
84        match value {
85            StringModificationError::InsertWouldExceedCapacity => {
86                SemanticStringError::ExceedsMaximumLength
87            }
88            StringModificationError::InvalidCharacter => SemanticStringError::InvalidContent,
89        }
90    }
91}
92
93impl core::fmt::Display for SemanticStringError {
94    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
95        core::write!(f, "SemanticStringError::{self:?}")
96    }
97}
98
99impl core::error::Error for SemanticStringError {}
100
101#[doc(hidden)]
102pub mod internal {
103    use super::*;
104
105    pub trait SemanticStringAccessor<const CAPACITY: usize> {
106        unsafe fn new_empty() -> Self;
107        unsafe fn get_mut_string(&mut self) -> &mut StaticString<CAPACITY>;
108        fn is_invalid_content(string: &[u8]) -> bool;
109        fn does_contain_invalid_characters(string: &[u8]) -> bool;
110    }
111}
112
113/// Trait that defines the methods a [`StaticString`] with context semantics, a
114/// [`SemanticString`] shares. A new [`SemanticString`] can be created with the [`crate::semantic_string!`]
115/// macro. For the usage, see [`mod@crate::semantic_string`].
116pub trait SemanticString<const CAPACITY: usize>:
117    internal::SemanticStringAccessor<CAPACITY>
118    + Debug
119    + Display
120    + Sized
121    + Deref<Target = [u8]>
122    + PartialEq
123    + Eq
124    + Hash
125    + Clone
126    + Copy
127{
128    /// Returns a reference to the underlying [`StaticString`]
129    fn as_string(&self) -> &StaticString<CAPACITY>;
130
131    /// Creates a new content. If it contains invalid characters or exceeds the maximum supported
132    /// length of the system or contains illegal strings it fails.
133    fn new(value: &[u8]) -> Result<Self, SemanticStringError> {
134        let msg = "Unable to create SemanticString";
135        let origin = "SemanticString::new()";
136
137        let mut new_self =
138            unsafe { <Self as internal::SemanticStringAccessor<CAPACITY>>::new_empty() };
139        fail!(from origin, when new_self.push_bytes(value),
140            "{} due to an invalid value \"{}\".", msg, as_escaped_string(value));
141
142        Ok(new_self)
143    }
144
145    /// Creates a new content but does not verify that it does not contain invalid characters.
146    ///
147    /// # Safety
148    ///
149    ///   * The slice must contain only valid characters.
150    ///   * The slice must have a length that is less or equal CAPACITY
151    ///   * The slice must not contain invalid UTF-8 characters
152    ///
153    unsafe fn new_unchecked(bytes: &[u8]) -> Self;
154
155    /// Creates a new content from a given ptr. The user has to ensure that it is null-terminated.
156    ///
157    /// # Safety
158    ///
159    ///   * The pointer must be '\0' (null) terminated
160    ///   * The pointer must be valid and non-null
161    ///   * The contents must have a length that is less or equal CAPACITY
162    ///   * The contents must not contain invalid UTF-8 characters
163    ///
164    unsafe fn from_c_str(ptr: *const core::ffi::c_char) -> Result<Self, SemanticStringError> {
165        unsafe {
166            Self::new(core::slice::from_raw_parts(
167                ptr.cast(),
168                strnlen(ptr, CAPACITY + 1),
169            ))
170        }
171    }
172
173    /// Returns the contents as a slice
174    fn as_bytes(&self) -> &[u8] {
175        self.as_string().as_bytes()
176    }
177
178    /// Returns a zero terminated slice of the underlying bytes
179    fn as_c_str(&self) -> *const core::ffi::c_char {
180        self.as_string().as_c_str()
181    }
182
183    /// Returns the capacity of the file system type
184    fn capacity(&self) -> usize {
185        CAPACITY
186    }
187
188    /// Finds the first occurrence of a  byte string in the given string. If the byte string was
189    /// found the start position of the byte string is returned, otherwise [`None`].
190    fn find(&self, bytes: &[u8]) -> Option<usize> {
191        self.as_string().find(bytes)
192    }
193
194    /// Finds the last occurrence of a byte string in the given string. If the byte string was
195    /// found the start position of the byte string is returned, otherwise [`None`].
196    fn rfind(&self, bytes: &[u8]) -> Option<usize> {
197        self.as_string().find(bytes)
198    }
199
200    /// Returns true when the string is full, otherwise false
201    fn is_full(&self) -> bool {
202        self.as_string().is_full()
203    }
204
205    /// Returns true when the string is empty, otherwise false
206    fn is_empty(&self) -> bool {
207        self.as_string().is_empty()
208    }
209
210    /// Returns the length of the string
211    fn len(&self) -> usize {
212        self.as_string().len()
213    }
214
215    /// Inserts a single byte at a specific position. When the capacity is exceeded, the byte is an
216    /// illegal character or the content would result in an illegal content it fails.
217    fn insert(&mut self, idx: usize, byte: u8) -> Result<(), SemanticStringError> {
218        self.insert_bytes(idx, &[byte; 1])
219    }
220
221    /// Inserts a byte slice at a specific position. When the capacity is exceeded, the byte slice contains
222    /// illegal characters or the content would result in an illegal content it fails.
223    fn insert_bytes(&mut self, idx: usize, bytes: &[u8]) -> Result<(), SemanticStringError> {
224        let msg = "Unable to insert byte string";
225        fail!(from self, when unsafe { self.get_mut_string().insert_bytes(idx, bytes) },
226                with SemanticStringError::ExceedsMaximumLength,
227                    "{} \"{}\" since it would exceed the maximum allowed length of {}.",
228                        msg, as_escaped_string(bytes), CAPACITY);
229
230        if Self::is_invalid_content(self.as_bytes()) {
231            unsafe { self.get_mut_string().remove_range(idx, bytes.len()) };
232            fail!(from self, with SemanticStringError::InvalidContent,
233                "{} \"{}\" since it would result in an illegal content.",
234                msg, as_escaped_string(bytes));
235        }
236
237        Ok(())
238    }
239
240    /// Adds bytes to the string without checking if they only contain valid characters or
241    /// would result in a valid result.
242    ///
243    /// # Safety
244    ///
245    ///   * The user must ensure that the bytes contain only valid characters.
246    ///   * The user must ensure that the result, after the bytes were added, is valid.
247    ///   * The slice must have a length that is less or equal CAPACITY
248    ///   * The slice is not contain invalid UTF-8 characters
249    ///
250    unsafe fn insert_bytes_unchecked(&mut self, idx: usize, bytes: &[u8]);
251
252    /// Normalizes the string. This function is used as basis for [`core::hash::Hash`] and
253    /// [`PartialEq`]. Normalizing a [`SemanticString`] means to bring it to some format so that it
254    /// contains still the same semantic content but in an uniform way so that strings, with the
255    /// same semantic content but different representation compare as equal.
256    fn normalize(&self) -> Self;
257
258    /// Removes the last character. If the string is empty it returns [`None`].
259    /// If the removal would create an illegal content it fails.
260    fn pop(&mut self) -> Result<Option<u8>, SemanticStringError> {
261        if self.len() == 0 {
262            return Ok(None);
263        }
264
265        self.remove(self.len() - 1)
266    }
267
268    /// Adds a single byte at the end. When the capacity is exceeded, the byte is an
269    /// illegal character or the content would result in an illegal content it fails.
270    fn push(&mut self, byte: u8) -> Result<(), SemanticStringError> {
271        self.insert(self.len(), byte)
272    }
273
274    /// Adds a byte slice at the end. When the capacity is exceeded, the byte slice contains
275    /// illegal characters or the content would result in an illegal content it fails.
276    fn push_bytes(&mut self, bytes: &[u8]) -> Result<(), SemanticStringError> {
277        self.insert_bytes(self.len(), bytes)
278    }
279
280    /// Removes a byte at a specific position and returns it.
281    /// If the removal would create an illegal content it fails.
282    fn remove(&mut self, idx: usize) -> Result<Option<u8>, SemanticStringError> {
283        let mut temp = *self.as_string();
284        let value = temp.remove(idx);
285
286        if Self::is_invalid_content(temp.as_bytes()) {
287            fail!(from self, with SemanticStringError::InvalidContent,
288                "Unable to remove character at position {} since it would result in an illegal content.",
289                idx);
290        }
291
292        unsafe { *self.get_mut_string() = temp };
293        Ok(value)
294    }
295
296    /// Removes a range.
297    /// If the removal would create an illegal content it fails.
298    fn remove_range(&mut self, idx: usize, len: usize) -> Result<(), SemanticStringError> {
299        let mut temp = *self.as_string();
300        temp.remove_range(idx, len);
301        if Self::is_invalid_content(temp.as_bytes()) {
302            fail!(from self, with SemanticStringError::InvalidContent,
303                "Unable to remove range from {} with lenght {} since it would result in the illegal content \"{}\".",
304                    idx, len, temp);
305        }
306
307        unsafe { self.get_mut_string().remove_range(idx, len) };
308        Ok(())
309    }
310
311    /// Removes all bytes which satisfy the provided clojure f.
312    /// If the removal would create an illegal content it fails.
313    fn retain<F: FnMut(u8) -> bool>(&mut self, f: F) -> Result<(), SemanticStringError> {
314        let mut temp = *self.as_string();
315        temp.retain(f);
316
317        if Self::is_invalid_content(temp.as_bytes()) {
318            fail!(from self, with SemanticStringError::InvalidContent,
319                "Unable to retain characters from string since it would result in the illegal content \"{}\".",
320                temp);
321        }
322
323        unsafe { *self.get_mut_string() = temp };
324
325        Ok(())
326    }
327
328    /// Removes a prefix. If the prefix does not exist it returns false. If the removal would lead
329    /// to an invalid string content it fails and returns [`SemanticStringError::InvalidContent`].
330    /// After a successful removal it returns true.
331    fn strip_prefix(&mut self, bytes: &[u8]) -> Result<bool, SemanticStringError> {
332        let mut temp = *self.as_string();
333        if !temp.strip_prefix(bytes) {
334            return Ok(false);
335        }
336
337        if Self::is_invalid_content(temp.as_bytes()) {
338            let mut prefix = StaticString::<123>::new();
339            unsafe { prefix.insert_bytes_unchecked(0, bytes) };
340            fail!(from self, with SemanticStringError::InvalidContent,
341                "Unable to strip prefix \"{}\" from string since it would result in the illegal content \"{}\".",
342                prefix, temp);
343        }
344
345        unsafe { self.get_mut_string().strip_prefix(bytes) };
346
347        Ok(true)
348    }
349
350    /// Removes a suffix. If the suffix does not exist it returns false. If the removal would lead
351    /// to an invalid string content it fails and returns [`SemanticStringError::InvalidContent`].
352    /// After a successful removal it returns true.
353    fn strip_suffix(&mut self, bytes: &[u8]) -> Result<bool, SemanticStringError> {
354        let mut temp = *self.as_string();
355        if !temp.strip_suffix(bytes) {
356            return Ok(false);
357        }
358
359        if Self::is_invalid_content(temp.as_bytes()) {
360            let mut prefix = StaticString::<123>::new();
361            unsafe { prefix.insert_bytes_unchecked(0, bytes) };
362            fail!(from self, with SemanticStringError::InvalidContent,
363                "Unable to strip prefix \"{}\" from string since it would result in the illegal content \"{}\".",
364                prefix, temp);
365        }
366
367        unsafe { self.get_mut_string().strip_suffix(bytes) };
368
369        Ok(true)
370    }
371
372    /// Truncates the string to new_len.
373    fn truncate(&mut self, new_len: usize) -> Result<(), SemanticStringError> {
374        let mut temp = *self.as_string();
375        temp.truncate(new_len);
376
377        if Self::is_invalid_content(temp.as_bytes()) {
378            fail!(from self, with SemanticStringError::InvalidContent,
379                "Unable to truncate characters to {} since it would result in the illegal content \"{}\".",
380                new_len, temp);
381        }
382
383        unsafe { self.get_mut_string().truncate(new_len) };
384        Ok(())
385    }
386}
387
388/// Helper macro to create a new [`SemanticString`]. Usage example can be found here:
389/// [`mod@crate::semantic_string`].
390#[macro_export(local_inner_macros)]
391macro_rules! semantic_string {
392    {$(#[$documentation:meta])*
393     /// Name of the struct
394     name: $string_name:ident,
395     /// Capacity of the underlying StaticString
396     capacity: $capacity:expr,
397     /// Callable that gets a [`&[u8]`] as input and shall return true when the slice contains
398     /// invalid content.
399     invalid_content: $invalid_content:expr,
400     /// Callable that gets a [`&[u8]`] as input and shall return true when the slice contains
401     /// invalid characters.
402     invalid_characters: $invalid_characters:expr,
403     /// Normalizes the content. Required when the same semantical content has multiple
404     /// representations like paths for instance (`/tmp` == `/tmp/`)
405     normalize: $normalize:expr} => {
406        $(#[$documentation])*
407        #[repr(C)]
408        #[derive(Debug, Clone, Copy, Eq, PartialOrd, Ord, ZeroCopySend)]
409        pub struct $string_name {
410            value: iceoryx2_bb_container::string::StaticString<$capacity>
411        }
412
413        // BEGIN: serde
414        pub(crate) mod VisitorType {
415            pub(crate) struct $string_name;
416        }
417
418        impl<'de> serde::de::Visitor<'de> for VisitorType::$string_name {
419            type Value = $string_name;
420
421            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
422                formatter.write_str("a string containing the service name")
423            }
424
425            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
426            where
427                E: serde::de::Error,
428            {
429                match $string_name::new(v.as_bytes()) {
430                    Ok(v) => Ok(v),
431                    Err(v) => Err(E::custom(alloc::format!("invalid {} provided {:?}.", core::stringify!($string_name), v))),
432                }
433            }
434        }
435
436        impl<'de> serde::Deserialize<'de> for $string_name {
437            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
438            where
439                D: serde::Deserializer<'de>,
440            {
441                deserializer.deserialize_str(VisitorType::$string_name)
442            }
443        }
444
445        impl serde::Serialize for $string_name {
446            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
447            where
448                S: serde::Serializer,
449            {
450                serializer.serialize_str(core::str::from_utf8(self.as_bytes()).unwrap())
451            }
452        }
453        // END: serde
454
455        impl iceoryx2_bb_container::semantic_string::SemanticString<$capacity> for $string_name {
456            fn as_string(&self) -> &iceoryx2_bb_container::string::StaticString<$capacity> {
457                &self.value
458            }
459
460            fn normalize(&self) -> Self {
461                $normalize(self)
462            }
463
464            unsafe fn new_unchecked(bytes: &[u8]) -> Self {
465                Self {
466                    value: iceoryx2_bb_container::string::StaticString::from_bytes_unchecked(bytes),
467                }
468            }
469
470            unsafe fn insert_bytes_unchecked(&mut self, idx: usize, bytes: &[u8]) {
471                use iceoryx2_bb_container::string::String;
472                self.value.insert_bytes_unchecked(idx, bytes);
473            }
474        }
475
476        impl $string_name {
477            /// Creates a new instance.
478            ///
479            /// # Safety
480            ///
481            /// * The provided slice must have a length smaller or equal to the capacity. `value.len() < Self::max_len()`
482            /// * The contents of the slice must follow the content contract
483            ///
484            pub const unsafe fn new_unchecked_const(value: &[u8]) -> $string_name {
485                core::debug_assert!(value.len() <= $capacity);
486                $string_name {
487                    value: iceoryx2_bb_container::string::StaticString::from_bytes_unchecked(value),
488                }
489            }
490
491            /// Returns the maximum supported length
492            pub const fn max_len() -> usize {
493                $capacity
494            }
495
496            pub const fn as_bytes_const(&self) -> &[u8] {
497                self.value.as_bytes_const()
498            }
499        }
500
501        impl core::fmt::Display for $string_name {
502            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
503                core::write!(f, "{}", self.value)
504            }
505        }
506
507        impl Hash for $string_name {
508            fn hash<H: Hasher>(&self, state: &mut H) {
509                self.normalize().as_bytes().hash(state)
510            }
511        }
512
513        impl From<$string_name> for String {
514            fn from(value: $string_name) -> String {
515                // SAFETY: It is ensured that the semantic string contains only valid utf-8 strings
516                unsafe { String::from_utf8_unchecked(value.as_bytes().to_vec()) }
517            }
518        }
519
520        impl From<&$string_name> for String {
521            fn from(value: &$string_name) -> String {
522                // SAFETY: It is ensured that the semantic string contains only valid utf-8 strings
523                unsafe { String::from_utf8_unchecked(value.as_bytes().to_vec()) }
524            }
525        }
526
527        impl core::convert::TryFrom<&str> for $string_name {
528            type Error = iceoryx2_bb_container::semantic_string::SemanticStringError;
529
530            fn try_from(value: &str) -> Result<Self, Self::Error> {
531                Self::new(value.as_bytes())
532            }
533        }
534
535        impl PartialEq<$string_name> for $string_name {
536            fn eq(&self, other: &$string_name) -> bool {
537                *self.normalize().as_bytes() == *other.normalize().as_bytes()
538            }
539        }
540
541        impl PartialEq<&[u8]> for $string_name {
542            fn eq(&self, other: &&[u8]) -> bool {
543                let other = match $string_name::new(other) {
544                    Ok(other) => other,
545                    Err(_) => return false,
546                };
547
548                *self == other
549            }
550        }
551
552        impl PartialEq<&[u8]> for &$string_name {
553            fn eq(&self, other: &&[u8]) -> bool {
554                let other = match $string_name::new(other) {
555                    Ok(other) => other,
556                    Err(_) => return false,
557                };
558
559                **self == other
560            }
561        }
562
563        impl<const CAPACITY: usize> PartialEq<[u8; CAPACITY]> for $string_name {
564            fn eq(&self, other: &[u8; CAPACITY]) -> bool {
565                let other = match $string_name::new(other) {
566                    Ok(other) => other,
567                    Err(_) => return false,
568                };
569
570                *self == other
571            }
572        }
573
574        impl<const CAPACITY: usize> PartialEq<&[u8; CAPACITY]> for $string_name {
575            fn eq(&self, other: &&[u8; CAPACITY]) -> bool {
576                let other = match $string_name::new(*other) {
577                    Ok(other) => other,
578                    Err(_) => return false,
579                };
580
581                *self == other
582            }
583        }
584
585        impl PartialEq<&str> for &$string_name {
586            fn eq(&self, other: &&str) -> bool {
587                let other = match $string_name::new(other.as_bytes()) {
588                    Ok(other) => other,
589                    Err(_) => return false,
590                };
591
592                **self == other
593            }
594        }
595
596        impl core::ops::Deref for $string_name {
597            type Target = [u8];
598
599            fn deref(&self) -> &Self::Target {
600                use iceoryx2_bb_container::string::String;
601                self.value.as_bytes()
602            }
603        }
604
605        impl iceoryx2_bb_container::semantic_string::internal::SemanticStringAccessor<$capacity> for $string_name {
606            unsafe fn new_empty() -> Self {
607                Self {
608                    value: iceoryx2_bb_container::string::StaticString::new(),
609                }
610            }
611
612            unsafe fn get_mut_string(&mut self) -> &mut iceoryx2_bb_container::string::StaticString<$capacity> {
613                &mut self.value
614            }
615
616            fn is_invalid_content(string: &[u8]) -> bool {
617                if Self::does_contain_invalid_characters(string) {
618                    return true;
619                }
620
621                $invalid_content(string)
622            }
623
624            fn does_contain_invalid_characters(string: &[u8]) -> bool {
625                if core::str::from_utf8(string).is_err() {
626                    return true;
627                }
628
629                $invalid_characters(string)
630            }
631        }
632
633    };
634}