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