1use alloc::{
2 borrow::{Cow, ToOwned},
3 boxed::Box,
4 string::{String, ToString},
5 sync::Arc,
6};
7use core::{borrow::Borrow, fmt::Write as _, hash::Hash, str::FromStr};
8
9use crate::{
10 array::FixedArray,
11 inline::InlineString,
12 length::{InvalidStrLength, SmallLen, ValidLength},
13 r#static::StaticStr,
14};
15
16#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
17enum FixedStringRepr<LenT: ValidLength> {
18 Static(StaticStr<LenT>),
19 Heap(FixedArray<u8, LenT>),
20 Inline(InlineString<LenT::InlineStrRepr>),
21}
22
23#[cold]
24fn truncate_string(err: InvalidStrLength, max_len: usize) -> String {
25 let mut value = String::from(err.get_inner());
26 value.truncate(truncate_str(&value, max_len).len());
27 value
28}
29
30#[cold]
31fn truncate_str(string: &str, max_len: usize) -> &str {
32 for len in (0..=max_len).rev() {
33 if string.is_char_boundary(len) {
34 return &string[..len];
35 }
36 }
37
38 unreachable!("Len 0 is a char boundary")
39}
40
41#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
45pub struct FixedString<LenT: ValidLength = SmallLen>(FixedStringRepr<LenT>);
46
47impl<LenT: ValidLength> FixedString<LenT> {
48 #[must_use]
49 pub fn new() -> Self {
50 Self::from_static_trunc("")
51 }
52
53 pub(crate) fn new_inline(val: &str) -> Option<Self> {
54 InlineString::from_str(val)
55 .map(FixedStringRepr::Inline)
56 .map(Self)
57 }
58
59 pub fn from_static_trunc(val: &'static str) -> Self {
65 Self(FixedStringRepr::Static(StaticStr::from_static_str(
66 truncate_str(val, LenT::MAX.to_usize()),
67 )))
68 }
69
70 #[must_use]
82 pub fn from_str_trunc(val: &str) -> Self {
83 if let Some(inline) = Self::new_inline(val) {
84 inline
85 } else {
86 Self::from_string_trunc(val.to_owned())
87 }
88 }
89
90 #[must_use]
95 pub fn from_string_trunc(str: String) -> Self {
96 match str.into_boxed_str().try_into() {
97 Ok(val) => val,
98 Err(err) => Self::from_string_trunc(truncate_string(err, LenT::MAX.to_usize())),
99 }
100 }
101
102 #[must_use]
104 pub fn len(&self) -> LenT {
105 match &self.0 {
106 FixedStringRepr::Heap(a) => a.len(),
107 FixedStringRepr::Static(a) => a.len(),
108 FixedStringRepr::Inline(a) => a.len().into(),
109 }
110 }
111
112 #[must_use]
114 pub fn is_empty(&self) -> bool {
115 self.len() == LenT::ZERO
116 }
117
118 #[must_use]
120 pub fn as_str(&self) -> &str {
121 self
122 }
123
124 #[must_use]
126 pub fn into_string(self) -> String {
127 self.into()
128 }
129
130 #[cfg(test)]
131 #[must_use]
132 pub(crate) fn is_inline(&self) -> bool {
133 matches!(self, Self(FixedStringRepr::Inline(_)))
134 }
135
136 #[cfg(test)]
137 #[must_use]
138 pub(crate) fn is_static(&self) -> bool {
139 matches!(self, Self(FixedStringRepr::Static(_)))
140 }
141}
142
143impl<LenT: ValidLength> core::ops::Deref for FixedString<LenT> {
144 type Target = str;
145
146 fn deref(&self) -> &Self::Target {
147 match &self.0 {
148 FixedStringRepr::Heap(a) => unsafe { core::str::from_utf8_unchecked(a) },
150 FixedStringRepr::Static(a) => a.as_str(),
151 FixedStringRepr::Inline(a) => a.as_str(),
152 }
153 }
154}
155
156impl<LenT: ValidLength> Default for FixedString<LenT> {
157 fn default() -> Self {
158 FixedString::new()
159 }
160}
161
162impl<LenT: ValidLength> Clone for FixedString<LenT> {
163 fn clone(&self) -> Self {
164 match &self.0 {
165 FixedStringRepr::Heap(a) => Self(FixedStringRepr::Heap(a.clone())),
166 FixedStringRepr::Inline(a) => Self(FixedStringRepr::Inline(*a)),
167 FixedStringRepr::Static(a) => Self(FixedStringRepr::Static(*a)),
168 }
169 }
170
171 fn clone_from(&mut self, source: &Self) {
172 match (&mut self.0, &source.0) {
173 (FixedStringRepr::Heap(new), FixedStringRepr::Heap(src)) => new.clone_from(src),
174 #[allow(clippy::assigning_clones)]
175 _ => *self = source.clone(),
176 }
177 }
178}
179
180impl<LenT: ValidLength> Hash for FixedString<LenT> {
181 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
182 self.as_str().hash(state);
183 }
184}
185
186impl<LenT: ValidLength> PartialEq for FixedString<LenT> {
187 fn eq(&self, other: &Self) -> bool {
188 self.as_str() == other.as_str()
189 }
190}
191
192impl<LenT: ValidLength> Eq for FixedString<LenT> {}
193
194impl<LenT: ValidLength> PartialEq<String> for FixedString<LenT> {
195 fn eq(&self, other: &String) -> bool {
196 self.as_str().eq(other)
197 }
198}
199
200impl<LenT: ValidLength> PartialEq<&str> for FixedString<LenT> {
201 fn eq(&self, other: &&str) -> bool {
202 self.as_str().eq(*other)
203 }
204}
205
206impl<LenT: ValidLength> PartialEq<str> for FixedString<LenT> {
207 fn eq(&self, other: &str) -> bool {
208 self.as_str().eq(other)
209 }
210}
211
212impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for &str {
213 fn eq(&self, other: &FixedString<LenT>) -> bool {
214 other == self
215 }
216}
217
218impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for str {
219 fn eq(&self, other: &FixedString<LenT>) -> bool {
220 other == self
221 }
222}
223
224impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for String {
225 fn eq(&self, other: &FixedString<LenT>) -> bool {
226 other == self
227 }
228}
229
230impl<LenT: ValidLength> core::cmp::PartialOrd for FixedString<LenT> {
231 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
232 Some(self.cmp(other))
233 }
234}
235
236impl<LenT: ValidLength> core::cmp::Ord for FixedString<LenT> {
237 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
238 self.as_str().cmp(other.as_str())
239 }
240}
241
242impl<LenT: ValidLength> core::fmt::Display for FixedString<LenT> {
243 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244 f.write_str(self)
245 }
246}
247
248impl<LenT: ValidLength> core::fmt::Debug for FixedString<LenT> {
249 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
250 f.write_char('"')?;
251 f.write_str(self)?;
252 f.write_char('"')
253 }
254}
255
256impl<LenT: ValidLength> FromStr for FixedString<LenT> {
257 type Err = InvalidStrLength;
258
259 fn from_str(val: &str) -> Result<Self, Self::Err> {
260 if let Some(inline) = Self::new_inline(val) {
261 Ok(inline)
262 } else {
263 Self::try_from(Box::from(val))
264 }
265 }
266}
267
268impl<LenT: ValidLength> TryFrom<Box<str>> for FixedString<LenT> {
269 type Error = InvalidStrLength;
270
271 fn try_from(value: Box<str>) -> Result<Self, Self::Error> {
272 if let Some(inline) = Self::new_inline(&value) {
273 return Ok(inline);
274 }
275
276 match value.into_boxed_bytes().try_into() {
277 Ok(val) => Ok(Self(FixedStringRepr::Heap(val))),
278 Err(err) => Err(err
279 .try_into()
280 .expect("Box<str> -> Box<[u8]> should stay valid UTF8")),
281 }
282 }
283}
284
285impl<LenT: ValidLength> TryFrom<String> for FixedString<LenT> {
286 type Error = InvalidStrLength;
287
288 fn try_from(value: String) -> Result<Self, Self::Error> {
289 if let Some(inline) = Self::new_inline(&value) {
290 return Ok(inline);
291 }
292
293 value.into_boxed_str().try_into()
294 }
295}
296
297impl<LenT: ValidLength> From<char> for FixedString<LenT> {
298 fn from(value: char) -> Self {
299 use alloc::vec;
300
301 if let Some(value) = InlineString::from_char(value) {
302 return Self(FixedStringRepr::Inline(value));
303 }
304
305 let mut bytes = vec![0; value.len_utf8()].into_boxed_slice();
306
307 value.encode_utf8(&mut bytes);
308
309 let bytes = bytes
310 .try_into()
311 .expect("len_utf8 is at most 4, so it will fit in u8");
312
313 Self(FixedStringRepr::Heap(bytes))
314 }
315}
316
317impl<LenT: ValidLength> From<FixedString<LenT>> for String {
318 fn from(value: FixedString<LenT>) -> Self {
319 match value.0 {
320 FixedStringRepr::Heap(a) => unsafe { String::from_utf8_unchecked(a.into()) },
322 FixedStringRepr::Inline(a) => a.as_str().to_string(),
323 FixedStringRepr::Static(a) => a.as_str().to_string(),
324 }
325 }
326}
327
328impl<'a, LenT: ValidLength> From<&'a FixedString<LenT>> for Cow<'a, str> {
329 fn from(value: &'a FixedString<LenT>) -> Self {
330 Cow::Borrowed(value.as_str())
331 }
332}
333
334impl<LenT: ValidLength> From<FixedString<LenT>> for Cow<'_, str> {
335 fn from(value: FixedString<LenT>) -> Self {
336 Cow::Owned(value.into_string())
337 }
338}
339
340impl<LenT: ValidLength> AsRef<str> for FixedString<LenT> {
341 fn as_ref(&self) -> &str {
342 self
343 }
344}
345
346impl<LenT: ValidLength> Borrow<str> for FixedString<LenT> {
347 fn borrow(&self) -> &str {
348 self
349 }
350}
351
352#[cfg(feature = "std")]
353impl<LenT: ValidLength> AsRef<std::path::Path> for FixedString<LenT> {
354 fn as_ref(&self) -> &std::path::Path {
355 self.as_str().as_ref()
356 }
357}
358
359#[cfg(feature = "std")]
360impl<LenT: ValidLength> AsRef<std::ffi::OsStr> for FixedString<LenT> {
361 fn as_ref(&self) -> &std::ffi::OsStr {
362 self.as_str().as_ref()
363 }
364}
365
366impl<LenT: ValidLength> From<FixedString<LenT>> for Arc<str> {
367 fn from(value: FixedString<LenT>) -> Self {
368 Arc::from(value.into_string())
369 }
370}
371
372#[cfg(feature = "to-arraystring")]
373impl to_arraystring::ToArrayString for &FixedString<u8> {
374 const MAX_LENGTH: usize = 255;
375 type ArrayString = to_arraystring::ArrayString<255>;
376
377 fn to_arraystring(self) -> Self::ArrayString {
378 Self::ArrayString::from(self).unwrap()
379 }
380}
381
382#[cfg(feature = "serde")]
383impl<'de, LenT: ValidLength> serde::Deserialize<'de> for FixedString<LenT> {
384 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
385 use core::marker::PhantomData;
386
387 struct Visitor<LenT: ValidLength>(PhantomData<LenT>);
388
389 impl<LenT: ValidLength> serde::de::Visitor<'_> for Visitor<LenT> {
390 type Value = FixedString<LenT>;
391
392 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
393 write!(formatter, "a string up to {} bytes long", LenT::MAX)
394 }
395
396 fn visit_str<E: serde::de::Error>(self, val: &str) -> Result<Self::Value, E> {
397 FixedString::from_str(val).map_err(E::custom)
398 }
399
400 fn visit_string<E: serde::de::Error>(self, val: String) -> Result<Self::Value, E> {
401 FixedString::try_from(val.into_boxed_str()).map_err(E::custom)
402 }
403 }
404
405 deserializer.deserialize_string(Visitor(PhantomData))
406 }
407}
408
409#[cfg(feature = "serde")]
410impl<LenT: ValidLength> serde::Serialize for FixedString<LenT> {
411 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
412 self.as_str().serialize(serializer)
413 }
414}
415
416#[cfg(test)]
417mod test {
418 use super::*;
419
420 fn check_u8_roundtrip_generic(to_fixed: fn(String) -> FixedString<u8>) {
421 for i in 0..=u8::MAX {
422 let original = "a".repeat(i.into());
423 let fixed = to_fixed(original);
424
425 assert!(fixed.bytes().all(|c| c == b'a'));
426 assert_eq!(fixed.len(), i);
427
428 if !fixed.is_static() {
429 assert_eq!(fixed.is_inline(), fixed.len() <= 9);
430 }
431 }
432 }
433
434 #[test]
435 fn test_truncating_behaviour() {
436 const STR: &str = "______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________🦀";
437
438 let string = FixedString::<u8>::from_static_trunc(STR);
439
440 let str = std::str::from_utf8(string.as_bytes()).expect("is utf8");
441
442 assert_eq!(str, string.as_str());
443 assert_ne!(STR, str);
444 }
445
446 #[test]
447 fn check_u8_roundtrip() {
448 check_u8_roundtrip_generic(|original| {
449 FixedString::<u8>::try_from(original.into_boxed_str()).unwrap()
450 });
451 }
452
453 #[test]
454 fn check_u8_roundtrip_static() {
455 check_u8_roundtrip_generic(|original| {
456 let static_str = Box::leak(original.into_boxed_str());
457 FixedString::from_static_trunc(static_str)
458 });
459 }
460
461 #[test]
462 #[cfg(feature = "serde")]
463 fn check_u8_roundtrip_serde() {
464 check_u8_roundtrip_generic(|original| {
465 serde_json::from_str(&alloc::format!("\"{original}\"")).unwrap()
466 });
467 }
468
469 #[test]
470 #[cfg(feature = "to-arraystring")]
471 fn check_u8_roundtrip_arraystring() {
472 use to_arraystring::ToArrayString;
473
474 check_u8_roundtrip_generic(|original| {
475 FixedString::from_str_trunc(
476 FixedString::from_string_trunc(original)
477 .to_arraystring()
478 .as_str(),
479 )
480 });
481 }
482
483 #[test]
484 fn check_sizes() {
485 type DoubleOpt<T> = Option<Option<T>>;
486
487 assert_eq!(core::mem::size_of::<Option<InlineString<[u8; 11]>>>(), 12);
488 assert_eq!(core::mem::align_of::<Option<InlineString<[u8; 11]>>>(), 1);
489 assert_eq!(core::mem::size_of::<Option<FixedArray<u8, u32>>>(), 12);
490 assert_eq!(core::mem::size_of::<DoubleOpt<FixedArray<u8, u32>>>(), 13);
492 assert_eq!(core::mem::align_of::<Option<FixedArray<u8, u32>>>(), 1);
493 assert_eq!(core::mem::size_of::<FixedStringRepr<u32>>(), 13);
495 assert_eq!(core::mem::align_of::<FixedStringRepr<u32>>(), 1);
496 }
497
498 #[test]
499 fn from_char_u8() {
500 let s: FixedString<u8> = 'a'.into();
501 assert_eq!(s.len(), 1);
502 assert!(s.is_inline());
503
504 let s: FixedString<u8> = '¼'.into();
505 assert_eq!(s.len(), 2);
506 assert!(s.is_inline());
507
508 let s: FixedString<u8> = 'âš¡'.into();
509 assert_eq!(s.len(), 3);
510 assert!(s.is_inline());
511
512 let s: FixedString<u8> = '🦀'.into();
513 assert_eq!(s.len(), 4);
514 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
515 assert!(s.is_inline());
516 }
517
518 #[test]
519 fn from_char_u16() {
520 let s: FixedString<u16> = 'a'.into();
521 assert_eq!(s.len(), 1);
522 assert!(s.is_inline());
523
524 let s: FixedString<u16> = '¼'.into();
525 assert_eq!(s.len(), 2);
526 assert!(s.is_inline());
527
528 let s: FixedString<u16> = 'âš¡'.into();
529 assert_eq!(s.len(), 3);
530 assert!(s.is_inline());
531
532 let s: FixedString<u16> = '🦀'.into();
533 assert_eq!(s.len(), 4);
534 assert!(s.is_inline());
535 }
536
537 #[test]
538 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
539 fn from_char_u32() {
540 let s: FixedString<u32> = 'a'.into();
541 assert_eq!(s.len(), 1);
542 assert!(s.is_inline());
543
544 let s: FixedString<u32> = '¼'.into();
545 assert_eq!(s.len(), 2);
546 assert!(s.is_inline());
547
548 let s: FixedString<u32> = 'âš¡'.into();
549 assert_eq!(s.len(), 3);
550 assert!(s.is_inline());
551
552 let s: FixedString<u32> = '🦀'.into();
553 assert_eq!(s.len(), 4);
554 assert!(s.is_inline());
555 }
556}