Skip to main content

northstar_runtime/common/
non_nul_string.rs

1use serde::{Deserialize, Serialize, Serializer};
2use std::{
3    convert::{TryFrom, TryInto},
4    ffi::CString,
5    fmt::{self, Formatter},
6    ops::Deref,
7    path::Path,
8};
9use thiserror::Error;
10use validator::ValidateLength;
11
12/// String that does not contain null bytes
13#[derive(Clone, Eq, PartialOrd, Ord, PartialEq, Hash)]
14pub struct NonNulString(String);
15
16impl NonNulString {
17    /// Returns the underlying string
18    pub fn as_str(&self) -> &str {
19        self.0.as_str()
20    }
21
22    /// Wrap a str in a NonNulString without any validation
23    ///
24    /// # Safety
25    /// This is unsafe because the string must not contain nul bytes.
26    pub unsafe fn from_str_unchecked(s: &str) -> Self {
27        Self(s.to_string())
28    }
29
30    /// Wrap a string in a NonNulString without any validation
31    ///
32    /// # Safety
33    /// This is unsafe because the string must not contain nul bytes.
34    pub unsafe fn from_string_unchecked(s: String) -> Self {
35        Self(s)
36    }
37}
38
39/// Null byte error
40#[derive(Error, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
41#[error("invalid null byte in string")]
42pub struct InvalidNulChar(usize);
43
44impl InvalidNulChar {
45    /// Returns the index of the null byte
46    pub fn pos(&self) -> usize {
47        self.0
48    }
49}
50
51impl fmt::Display for NonNulString {
52    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
53        write!(f, "{}", self.0)
54    }
55}
56
57impl fmt::Debug for NonNulString {
58    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
59        write!(f, "\"{}\"", self.0)
60    }
61}
62
63impl AsRef<[u8]> for NonNulString {
64    fn as_ref(&self) -> &[u8] {
65        self.0.as_bytes()
66    }
67}
68
69impl AsRef<str> for NonNulString {
70    fn as_ref(&self) -> &str {
71        &self.0
72    }
73}
74
75impl AsRef<Path> for NonNulString {
76    fn as_ref(&self) -> &Path {
77        Path::new(self.0.as_str())
78    }
79}
80
81impl Deref for NonNulString {
82    type Target = str;
83
84    fn deref(&self) -> &Self::Target {
85        &self.0
86    }
87}
88
89impl From<NonNulString> for CString {
90    fn from(s: NonNulString) -> Self {
91        unsafe { CString::from_vec_unchecked(s.0.into()) }
92    }
93}
94
95impl From<NonNulString> for String {
96    fn from(s: NonNulString) -> String {
97        s.0
98    }
99}
100
101impl TryFrom<String> for NonNulString {
102    type Error = InvalidNulChar;
103
104    fn try_from(value: String) -> Result<Self, Self::Error> {
105        if let Some(pos) = memchr::memchr(b'\0', value.as_bytes()) {
106            Err(InvalidNulChar(pos))
107        } else {
108            Ok(NonNulString(value))
109        }
110    }
111}
112
113impl TryFrom<&str> for NonNulString {
114    type Error = InvalidNulChar;
115
116    fn try_from(value: &str) -> Result<Self, Self::Error> {
117        value.to_string().try_into()
118    }
119}
120
121impl Serialize for NonNulString {
122    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
123    where
124        S: Serializer,
125    {
126        serializer.serialize_str(&self.0)
127    }
128}
129
130impl<'de> Deserialize<'de> for NonNulString {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: serde::Deserializer<'de>,
134    {
135        struct Visitor;
136
137        impl serde::de::Visitor<'_> for Visitor {
138            type Value = NonNulString;
139
140            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
141                formatter.write_str("string without nul bytes")
142            }
143
144            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
145                v.try_into().map_err(|_| E::custom("invalid string"))
146            }
147        }
148
149        deserializer.deserialize_str(Visitor)
150    }
151}
152
153/// Implement HasLen for NonNulString to allow validation of the length.
154impl ValidateLength<u64> for &NonNulString {
155    fn length(&self) -> Option<u64> {
156        Some(self.0.len() as u64)
157    }
158}
159
160#[test]
161fn try_from() {
162    assert!(NonNulString::try_from("hello").is_ok());
163    assert!(NonNulString::try_from("hello🤔").is_ok());
164}
165
166#[test]
167fn try_from_with_nul() {
168    assert!(NonNulString::try_from("hel\0lo").is_err());
169    assert!(NonNulString::try_from("\0hello").is_err());
170    assert!(NonNulString::try_from("hello\0").is_err());
171}
172
173#[test]
174#[allow(clippy::unwrap_used)]
175fn serialize() {
176    assert!(matches!(
177        serde_json::to_string(&NonNulString::try_from("hello").unwrap()),
178        Ok(s) if s == "\"hello\""
179    ));
180}
181
182#[test]
183#[allow(clippy::unwrap_used)]
184fn deserialize() {
185    assert!(matches!(
186        serde_json::from_str::<NonNulString>("\"hello\""),
187        Ok(n) if n == NonNulString::try_from("hello").unwrap()
188    ));
189    assert!(serde_json::from_str::<NonNulString>("\"a\0\"").is_err());
190}
191
192#[test]
193fn deserialize_with_nul() {
194    assert!(serde_json::from_str::<NonNulString>("\"hel\0lo\"").is_err());
195}