1use core::{fmt, str};
2
3#[derive(Clone, PartialEq, Eq)]
27pub struct BufStr<const N: usize> {
28 pub bytes: [u8; N],
30 pub len: u16,
32}
33
34impl<const N: usize> Default for BufStr<N> {
35 #[inline(always)]
36 fn default() -> Self {
37 Self {
38 bytes: [0; N],
39 len: 0,
40 }
41 }
42}
43
44impl<const N: usize> BufStr<N> {
45 pub const SIZE: usize = N;
47
48 #[inline]
53 pub fn new(s: &str) -> Self {
54 let mut bytes = [0u8; N];
55 let len = copy_str_prefix(&mut bytes, s, N) as u16;
56 Self { bytes, len }
57 }
58
59 #[inline]
70 pub fn from_bytes(bytes: &[u8]) -> Self {
71 let mut arr = [0u8; N];
72 let copy_len = bytes.len().min(N);
73 arr[..copy_len].copy_from_slice(&bytes[..copy_len]);
74 let len = arr[..copy_len]
75 .iter()
76 .position(|&b| b == 0)
77 .unwrap_or(copy_len);
78 Self {
79 bytes: arr,
80 len: len as u16,
81 }
82 }
83
84 #[inline(always)]
86 pub const fn len(&self) -> usize {
87 let len = self.len as usize;
88 if len < N { len } else { N }
89 }
90
91 #[inline(always)]
93 pub const fn is_empty(&self) -> bool {
94 self.len == 0
95 }
96
97 #[inline]
106 pub fn as_str(&self) -> &str {
107 match str::from_utf8(self.as_bytes()) {
108 Ok(s) => s,
109 Err(e) => handle_invalid_utf8(self.as_bytes(), e),
110 }
111 }
112
113 #[inline(always)]
115 pub fn as_bytes(&self) -> &[u8] {
116 &self.bytes[..self.len()]
117 }
118}
119
120impl<const N: usize> fmt::Write for BufStr<N> {
121 #[inline(never)]
122 fn write_str(&mut self, s: &str) -> fmt::Result {
123 let current = self.len();
124 let remaining = N.saturating_sub(current);
125 if remaining == 0 {
126 return Ok(());
127 }
128
129 let written = copy_str_prefix(&mut self.bytes[current..], s, remaining);
130 self.len = (current + written) as u16;
131 Ok(())
132 }
133}
134
135impl<const N: usize> fmt::Display for BufStr<N> {
136 #[inline(always)]
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(self.as_str())
139 }
140}
141
142impl<const N: usize> fmt::Debug for BufStr<N> {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(f, "{:?}", self.as_str())
145 }
146}
147
148#[cfg(feature = "serde")]
149impl<const N: usize> serde::Serialize for BufStr<N> {
150 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151 where
152 S: serde::Serializer,
153 {
154 self.as_str().serialize(serializer)
155 }
156}
157
158#[cfg(feature = "serde")]
159impl<'de, const N: usize> serde::Deserialize<'de> for BufStr<N> {
160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161 where
162 D: serde::Deserializer<'de>,
163 {
164 let s: &str = serde::Deserialize::deserialize(deserializer)?;
165 Ok(BufStr::new(s))
166 }
167}
168
169#[cfg(feature = "defmt")]
170impl<const N: usize> defmt::Format for BufStr<N> {
171 fn format(&self, f: defmt::Formatter) {
172 defmt::write!(f, "{}", self.as_str());
173 }
174}
175
176#[inline(never)]
182fn copy_str_prefix(dst: &mut [u8], src: &str, max_len: usize) -> usize {
183 let bytes = src.as_bytes();
184 let mut len = bytes.len().min(max_len).min(dst.len());
185 while !src.is_char_boundary(len) {
186 len -= 1;
187 }
188 if len != 0 {
189 dst[..len].copy_from_slice(&bytes[..len]);
190 }
191 len
192}
193
194#[cold]
195#[inline(never)]
196fn handle_invalid_utf8(slice: &[u8], e: core::str::Utf8Error) -> &str {
197 let valid = e.valid_up_to();
198 if valid == 0 {
199 "\u{FFFD}"
200 } else {
201 str::from_utf8(&slice[..valid]).unwrap_or("\u{FFFD}")
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn as_str_valid() {
211 let s = BufStr::<16>::new("hello");
212 assert_eq!(s.as_str(), "hello");
213 assert_eq!(s.len(), 5);
214 assert_eq!(BufStr::<8>::default().as_str(), "");
215 assert!(BufStr::<8>::default().is_empty());
216 }
217
218 #[test]
219 fn as_str_invalid_leading_byte() {
220 let s = BufStr::<8>::from_bytes(&[0xFF, b'a']);
221 assert_eq!(s.as_str(), "\u{FFFD}");
222 assert_eq!(s.len(), 2);
223 }
224
225 #[test]
226 fn as_str_valid_prefix_then_garbage() {
227 let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xFF, b'!']);
228 assert_eq!(s.as_str(), "hi");
229 assert_eq!(s.len(), 4);
230 }
231
232 #[test]
233 fn as_str_truncated_multibyte_at_start() {
234 let s = BufStr::<8>::from_bytes(&[0xE2, 0x82]);
236 assert_eq!(s.as_str(), "\u{FFFD}");
237 }
238
239 #[test]
240 fn as_str_truncated_multibyte_after_valid_prefix() {
241 let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xE2, 0x82]);
242 assert_eq!(s.as_str(), "hi");
243 }
244
245 #[test]
246 fn as_str_stops_at_nul() {
247 let s = BufStr::<8>::from_bytes(b"ab\0cd");
248 assert_eq!(s.as_str(), "ab");
249 assert_eq!(s.as_bytes(), b"ab");
250 assert_eq!(s.len(), 2);
251 }
252
253 #[test]
254 fn from_bytes_zero_padded() {
255 let mut raw = [0u8; 8];
257 raw[..5].copy_from_slice(b"hello");
258 let s = BufStr::<8>::from_bytes(&raw);
259 assert_eq!(s.as_str(), "hello");
260 assert_eq!(s.len(), 5);
261 }
262
263 #[test]
264 fn new_truncates_on_char_boundary() {
265 assert_eq!(BufStr::<2>::new("€").as_str(), "");
267 assert_eq!(BufStr::<2>::new("€").len(), 0);
268 assert_eq!(BufStr::<3>::new("€").as_str(), "€");
269 assert_eq!(BufStr::<3>::new("€").len(), 3);
270 assert_eq!(BufStr::<4>::new("a€b").as_str(), "a€");
271 assert_eq!(BufStr::<4>::new("a€b").len(), 4);
272 assert_eq!(BufStr::<5>::new("a€b").as_str(), "a€b");
273 assert_eq!(BufStr::<5>::new("a€b").len(), 5);
274 }
275
276 #[test]
277 fn write_str_appends_and_truncates() {
278 use core::fmt::Write;
279 let mut s = BufStr::<5>::default();
280 write!(&mut s, "hi").unwrap();
281 assert_eq!(s.len(), 2);
282 write!(&mut s, "€").unwrap(); assert_eq!(s.as_str(), "hi€");
284 assert_eq!(s.len(), 5);
285 write!(&mut s, "x").unwrap(); assert_eq!(s.as_str(), "hi€");
287 assert_eq!(s.len(), 5);
288 }
289}