smbioslib/core/
strings.rs

1use serde::{ser::SerializeSeq, Serialize, Serializer};
2use std::error;
3use std::{fmt, string::FromUtf8Error};
4
5/// # SMBIOS String-Set
6///
7/// The string-set part/section of an SMBIOS structure
8pub struct SMBiosStringSet {
9    strings: Vec<Vec<u8>>,
10    current_string_index: usize,
11}
12
13impl SMBiosStringSet {
14    /// Creates a new string-set section of a structure
15    pub fn new(string_area: Vec<u8>) -> SMBiosStringSet {
16        SMBiosStringSet {
17            strings: {
18                if string_area == &[] {
19                    vec![]
20                } else {
21                    string_area
22                        .split(|num| *num == 0)
23                        .into_iter()
24                        .map(|string_slice| string_slice.to_vec())
25                        .collect()
26                }
27            },
28            current_string_index: 0,
29        }
30    }
31
32    fn reset(&mut self) {
33        self.current_string_index = 0;
34    }
35
36    /// Returns a UTF-8 [String] at the given 1 based `index`
37    ///
38    /// If the index is 0 an empty string "" is returned.
39    /// If SMBiosStringError::InvalidStringNumber is returned, either the field value is corrupt or the string-set is corrupt.
40    /// If SMBiosStringError::Utf8 is returned, the string is corrupt.
41    pub fn get_string(&self, index: u8) -> SMBiosString {
42        let index_usize = index as usize;
43
44        // As of 3.5.0 DMTF has decided to make UTF-8 the standard for how to interpret strings.
45        //
46        // section 6.1.3:
47        // "Strings must be encoded as UTF-8 with no byte order mark (BOM). For compatibility
48        // with older SMBIOS parsers, US-ASCII characters should be used.
49        //
50        // When the formatted portion of an SMBIOS structure references a string, it does so by specifying
51        // a non-zero string number within the structure's string-set.
52        //
53        // If a string field references no string, a null (0) is placed in that string field."
54
55        // Referential transparency:
56        // In rust we can return the empty string ("") when index is 0. This is idempotent because
57        // the structure's string-set, by design, is incapable of producing an empty string.
58
59        SMBiosString {
60            value: match index_usize == 0 {
61                true => Ok(String::new()),
62                false => match index_usize <= self.strings.len() {
63                    true => String::from_utf8(self.strings[index_usize - 1].clone())
64                        .map_err(|err| err.into()),
65                    false => Err(SMBiosStringError::InvalidStringNumber(index)),
66                },
67            },
68        }
69    }
70
71    /// Iterates the raw bytes of the strings. The terminating 0 is not included in each string.
72    pub fn iter(&self) -> std::slice::Iter<'_, Vec<u8>> {
73        self.strings.iter()
74    }
75}
76
77impl Iterator for SMBiosStringSet {
78    type Item = SMBiosString;
79
80    fn next(&mut self) -> Option<Self::Item> {
81        if self.current_string_index == self.strings.len() {
82            self.reset();
83            return None;
84        }
85
86        let result = String::from_utf8(self.strings[self.current_string_index].clone())
87            .map_err(|err| err.into());
88
89        self.current_string_index = self.current_string_index + 1;
90
91        Some(SMBiosString::from(result))
92    }
93}
94
95impl IntoIterator for &SMBiosStringSet {
96    type Item = SMBiosString;
97    type IntoIter = SMBiosStringSet;
98
99    fn into_iter(self) -> Self::IntoIter {
100        SMBiosStringSet {
101            strings: self.strings.clone(),
102            current_string_index: 0,
103        }
104    }
105}
106
107impl fmt::Debug for SMBiosStringSet {
108    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
109        fmt.debug_list().entries(self.into_iter()).finish()
110    }
111}
112
113impl Serialize for SMBiosStringSet {
114    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: Serializer,
117    {
118        let strings: Vec<SMBiosString> = self.into_iter().collect();
119        let mut seq = serializer.serialize_seq(Some(strings.len()))?;
120        for e in strings {
121            match e.value {
122                Ok(val) => seq.serialize_element(&val)?,
123                Err(err) => seq.serialize_element(format!("{}", err).as_str())?,
124            }
125        }
126        seq.end()
127    }
128}
129
130/// # SMBiosStringError
131///
132/// An SMBIOS String retrival error
133#[derive(Serialize, Debug)]
134pub enum SMBiosStringError {
135    /// The structure's field is out of bounds of the formatted portion of the SMBIOS structure
136    FieldOutOfBounds,
137    /// The given string number was outside the range of the SMBIOS structure's string-set
138    InvalidStringNumber(u8),
139    /// UTF8 parsing error
140    #[serde(serialize_with = "ser_from_utf8_error")]
141    Utf8(FromUtf8Error),
142}
143
144fn ser_from_utf8_error<S>(data: &FromUtf8Error, serializer: S) -> Result<S::Ok, S::Error>
145where
146    S: Serializer,
147{
148    serializer.serialize_str(format!("{}", data).as_str())
149}
150
151impl fmt::Display for SMBiosStringError {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match *self {
154            SMBiosStringError::FieldOutOfBounds => {
155                write!(
156                    f,
157                    "The structure's field is out of bounds of the formatted portion of the SMBIOS structure"
158                )
159            }
160            SMBiosStringError::InvalidStringNumber(_) => {
161                write!(
162                    f,
163                    "The given string number was outside the range of the SMBIOS structure's string-set"
164                )
165            }
166            // The wrapped error contains additional information and is available
167            // via the source() method.
168            SMBiosStringError::Utf8(..) => {
169                write!(f, "UTF8 parsing error")
170            }
171        }
172    }
173}
174
175impl error::Error for SMBiosStringError {
176    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
177        match *self {
178            // The cause is the underlying implementation error type. Is implicitly
179            // cast to the trait object `&error::Error`. This works because the
180            // underlying type already implements the `Error` trait.
181            SMBiosStringError::Utf8(ref e) => Some(e),
182            _ => None,
183        }
184    }
185}
186
187// Implement the conversion from `FromUtf8Error` to `SMBiosStringError`.
188// This will be automatically called by `?` if a `FromUtf8Error`
189// needs to be converted into a `SMBiosStringError`.
190impl From<FromUtf8Error> for SMBiosStringError {
191    fn from(err: FromUtf8Error) -> SMBiosStringError {
192        SMBiosStringError::Utf8(err)
193    }
194}
195
196impl From<Result<String, SMBiosStringError>> for SMBiosString {
197    fn from(data: Result<String, SMBiosStringError>) -> Self {
198        SMBiosString { value: data }
199    }
200}
201
202/// # SMBiosString
203///
204/// Contains the retrival result for an SMBIOS string field.
205pub struct SMBiosString {
206    value: Result<String, SMBiosStringError>,
207}
208
209impl SMBiosString {
210    /// Produces a UTF-8 which includes invalid UTF-8 characters; otherwise, returns
211    /// Option::None for all other conditions.
212    pub fn to_utf8_lossy(&self) -> Option<String> {
213        match &self.value {
214            Ok(val) => Some(val.to_string()),
215            Err(err) => match err {
216                SMBiosStringError::Utf8(utf8) => {
217                    Some(String::from_utf8_lossy(utf8.as_bytes()).to_string())
218                }
219                _ => None,
220            },
221        }
222    }
223
224    /// Returns `true` if the result is [Ok].
225    pub const fn is_ok(&self) -> bool {
226        self.value.is_ok()
227    }
228
229    /// Returns `true` if the result is [Err].
230    pub const fn is_err(&self) -> bool {
231        self.value.is_err()
232    }
233
234    /// Converts to `Option<String>` consuming self, and discarding the error, if any.
235    pub fn ok(self) -> Option<String> {
236        self.value.ok()
237    }
238
239    /// Converts to `Option<SMBiosStringError>` consuming self, and discarding the success value, if any.
240    pub fn err(self) -> Option<SMBiosStringError> {
241        self.value.err()
242    }
243
244    /// Produces a new `Result`, containing a reference into the original, leaving the original in place.
245    pub const fn as_ref(&self) -> Result<&String, &SMBiosStringError> {
246        self.value.as_ref()
247    }
248
249    /// Converts to Result<&mut String, &mut SMBiosStringError>.
250    pub fn as_mut(&mut self) -> Result<&mut String, &mut SMBiosStringError> {
251        self.value.as_mut()
252    }
253}
254
255impl fmt::Display for SMBiosString {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match &self.value {
258            Ok(val) => write!(f, "{}", val),
259            Err(err) => write!(f, "{}", err),
260        }
261    }
262}
263
264impl fmt::Debug for SMBiosString {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match &self.value {
267            Ok(val) => write!(f, "{}", val),
268            Err(err) => write!(f, "{}", err),
269        }
270    }
271}
272
273impl Serialize for SMBiosString {
274    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
275    where
276        S: Serializer,
277    {
278        serializer.serialize_str(format!("{}", &self).as_str())
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_string_parsing() {
288        let string_set_bytes = vec![
289            // "en|US|iso8859-1"
290            0x65, 0x6E, 0x7C, 0x55, 0x53, 0x7C, 0x69, 0x73, 0x6F, 0x38, 0x38, 0x35, 0x39, 0x2D,
291            0x31, 0x00, // "Heart=💖"
292            b'H', b'e', b'a', b'r', b't', b'=', 240, 159, 146, 150, 0x00, // "Error="
293            b'E', b'r', b'r', b'o', b'r', b'=', 1, 159, 146, 150, 0x00,
294            // "ja|JP|unicode"
295            0x6A, 0x61, 0x7C, 0x4A, 0x50, 0x7C, 0x75, 0x6E, 0x69, 0x63, 0x6F, 0x64, 0x65,
296        ];
297
298        let string_set = SMBiosStringSet::new(string_set_bytes);
299
300        let mut string_iterator = string_set.into_iter();
301
302        let first_string = string_iterator.next().unwrap().value.unwrap();
303        assert_eq!(first_string, "en|US|iso8859-1".to_string());
304
305        let second_string = string_iterator.next().unwrap().value.unwrap();
306        assert_eq!(second_string, "Heart=💖".to_string());
307
308        // Err(FromUtf8Error { bytes: [69, 114, 114, 111, 114, 61, 1, 159, 146, 150], error: Utf8Error { valid_up_to: 7, error_len: Some(1) } })
309        match string_iterator.next().unwrap().value {
310            Ok(_) => panic!("This should have been a UTF8 error"),
311            Err(err) => match err {
312                SMBiosStringError::FieldOutOfBounds => panic!("This should have been inbounds"),
313                SMBiosStringError::InvalidStringNumber(_) => {
314                    panic!("This should have been a valid string number")
315                }
316                SMBiosStringError::Utf8(utf8) => {
317                    assert_eq!(7, utf8.utf8_error().valid_up_to());
318                    assert_eq!(
319                        "Error=\u{1}���",
320                        String::from_utf8_lossy(utf8.as_bytes()).to_string()
321                    );
322                }
323            },
324        }
325
326        let fourth_string = string_iterator.next().unwrap().value.unwrap();
327        assert_eq!(fourth_string, "ja|JP|unicode".to_string());
328    }
329}