smb_dtyp/binrw_util/
fixed_string.rs

1use std::{str::FromStr, string::FromUtf16Error};
2
3use binrw::prelude::*;
4
5/// Fixed-size string with a specified character type and length.
6///
7/// The string always takes up exactly N characters,
8/// with unused characters filled with default values of the character type.
9///
10/// Notes:
11/// - `From<&str>` trims or pads the string to fit exactly N characters.
12/// - `TryInto<String>` only pads with default values, it does not trim.
13#[binrw::binrw]
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BaseFixedString<C: Sized, const N: usize>
16where
17    C: BinRead + BinWrite + Copy + Clone + Default + 'static,
18    for<'a> <C as BinRead>::Args<'a>: Default + Clone,
19    for<'b> <C as BinWrite>::Args<'b>: Default + Clone,
20{
21    pub(crate) data: [C; N],
22}
23
24/// Fixed-size wide string with UTF-16 encoding.
25pub type FixedWideString<const N: usize> = BaseFixedString<u16, N>;
26
27/// Fixed-size ANSI string with UTF-8 encoding.
28pub type FixedAnsiString<const N: usize> = BaseFixedString<u8, N>;
29
30impl<C: Sized, const N: usize> BaseFixedString<C, N>
31where
32    C: BinRead + BinWrite + Copy + Clone + Default + 'static,
33    for<'a> <C as BinRead>::Args<'a>: Default + Clone,
34    for<'b> <C as BinWrite>::Args<'b>: Default + Clone,
35{
36    /// The size of the FixedString in memory, in bytes.
37    ///
38    /// This is also it's size when bin-read or bin-written.
39    pub const SIZE_BYTES: usize = N * std::mem::size_of::<C>();
40
41    /// The maximum number of characters in the FixedString.
42    ///
43    /// This is the very same as the generic const parameter N.
44    pub const MAX_CHARS: usize = N;
45
46    /// Creates a new FixedString from a slice.
47    ///
48    /// If the slice is shorter than N, the remaining bytes are filled with default values.
49    /// If the slice is longer than N, it is truncated.
50    pub fn from_slice(slice: &[C]) -> Self {
51        let mut data = [C::default(); N];
52        let len = slice.len().min(N);
53        data[..len].copy_from_slice(&slice[..len]);
54        Self { data }
55    }
56
57    /// Returns the inner data as a slice.
58    pub fn as_slice(&self) -> &[C] {
59        &self.data
60    }
61}
62
63impl<C: Sized, const N: usize> Default for BaseFixedString<C, N>
64where
65    C: BinRead + BinWrite + Copy + Clone + Default + 'static,
66    for<'a> <C as BinRead>::Args<'a>: Default + Clone,
67    for<'b> <C as BinWrite>::Args<'b>: Default + Clone,
68{
69    fn default() -> Self {
70        Self {
71            data: [C::default(); N],
72        }
73    }
74}
75
76impl<const N: usize> From<&str> for FixedAnsiString<N> {
77    fn from(s: &str) -> Self {
78        let bytes = s.as_bytes();
79        Self::from_slice(bytes)
80    }
81}
82
83macro_rules! same_generic_impls {
84    ($($chartype:ty)+) => {
85        $(
86
87impl<const N: usize> FromStr for BaseFixedString<$chartype, N> {
88    type Err = &'static str;
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        if s.len() > Self::MAX_CHARS {
91            return Err("Input string is longer than fixed size");
92        }
93        Ok(Self::from(s))
94    }
95}
96        )+
97    };
98}
99
100same_generic_impls!(u8 u16);
101
102impl<const N: usize> From<&str> for FixedWideString<N> {
103    fn from(s: &str) -> Self {
104        let wide: Vec<u16> = s.encode_utf16().collect();
105        Self::from_slice(&wide)
106    }
107}
108
109impl<const N: usize> TryInto<String> for FixedAnsiString<N> {
110    type Error = std::string::FromUtf8Error;
111    fn try_into(self) -> Result<String, Self::Error> {
112        String::from_utf8(self.as_slice().to_vec())
113    }
114}
115
116impl<const N: usize> TryInto<String> for FixedWideString<N> {
117    type Error = FromUtf16Error;
118    fn try_into(self) -> Result<String, Self::Error> {
119        String::from_utf16(self.as_slice())
120    }
121}
122
123impl<const N: usize> std::fmt::Display for FixedAnsiString<N> {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        let s = String::from_utf8_lossy(self.as_slice());
126        write!(f, "{}", s)
127    }
128}
129
130impl<const N: usize> std::fmt::Display for FixedWideString<N> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        super::sized_string::display_utf16(self.as_slice(), f, core::iter::once)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use smb_tests::*;
140
141    type Ansi6 = FixedAnsiString<6>;
142
143    /* From - sanity */
144
145    test_binrw! {
146        Ansi6 => A60: Ansi6::from("HelloA") => [72, 101, 108, 108, 111, 65]
147    }
148
149    test_binrw! {
150        Ansi6 => A61: Ansi6::from("Sh") => [83, 104, 0, 0, 0, 0]
151    }
152
153    test_binrw! {
154        Ansi6 => A62: Ansi6::from("") => [0, 0, 0, 0, 0, 0]
155    }
156
157    test_binrw! {
158        Ansi6 => A63: Ansi6::from("HelloALLLLLLLLLLLLLLLLL") => [72, 101, 108, 108, 111, 65]
159    }
160
161    type Wide6 = FixedWideString<6>;
162    test_binrw! {
163        Wide6 => W60: Wide6::from("HelloA") => [72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 65, 0]
164    }
165    test_binrw! {
166        Wide6 => W61: Wide6::from("Hi") => [72, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0]
167    }
168    test_binrw! {
169        Wide6 => W62: Wide6::from("") => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
170    }
171    test_binrw! {
172        Wide6 => W63: Wide6::from("HelloAWWWWWWWWWWWWWWWWWW") => [72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 65, 0]
173    }
174
175    macro_rules! fixed_string_tests {
176        ($($chartype:ty)+) => {
177            $(
178                pastey::paste! {
179    #[test]
180    fn [<test_fixed_string_size_bytes_ $chartype:lower>]() {
181        type FS = BaseFixedString<$chartype, 10>;
182        assert_eq!(FS::SIZE_BYTES, 10 * std::mem::size_of::<$chartype>());
183    }
184
185    #[test]
186    fn [<test_fixed_string_max_chars_ $chartype:lower>]() {
187        type FS = BaseFixedString<$chartype, 15>;
188        assert_eq!(FS::MAX_CHARS, 15);
189    }
190
191    /* TryFrom */
192    #[test]
193    fn [<test_fixed_string_try_from_str_ $chartype:lower>]() {
194        type FS = BaseFixedString<$chartype, 5>;
195        let s = "abc";
196        let fs: FS = s.parse().unwrap();
197        assert_eq!(fs.as_slice()[0], 'a' as $chartype);
198        assert_eq!(fs.as_slice()[1], 'b' as $chartype);
199        assert_eq!(fs.as_slice()[2], 'c' as $chartype);
200        for &c in &fs.as_slice()[3..] {
201            assert_eq!(c, <$chartype>::default());
202        }
203    }
204
205    #[test]
206    fn [<test_fixed_string_try_from_str_too_long_ $chartype:lower>]() {
207        type FS = BaseFixedString<$chartype, 3>;
208        let s = "abcd";
209        let result: Result<FS, _> = s.parse();
210        assert!(result.is_err());
211    }
212
213    type [<TemplatedTest $chartype:camel 6>] = BaseFixedString<$chartype, 6>;
214
215    test_binrw_read_fail! {
216        [<TemplatedTest $chartype:camel 6>]: [255, 255, 255]
217    }
218                }
219            )+
220        };
221    }
222
223    fixed_string_tests! { u8 u16 }
224}