1use alloc::{
2 borrow::{Cow, ToOwned},
3 boxed::Box,
4 string::String,
5 sync::Arc,
6};
7use core::{borrow::Borrow, 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(mut val: &'static str) -> Self {
65 let max_len = LenT::MAX.to_usize();
66 if val.len() > max_len {
67 val = truncate_str(val, max_len);
68 }
69
70 Self(FixedStringRepr::Static(StaticStr::from_static_str(val)))
71 }
72
73 #[must_use]
85 pub fn from_str_trunc(val: &str) -> Self {
86 if let Some(inline) = Self::new_inline(val) {
87 inline
88 } else {
89 Self::from_string_trunc(val.to_owned())
90 }
91 }
92
93 #[must_use]
98 pub fn from_string_trunc(str: String) -> Self {
99 match str.try_into() {
100 Ok(val) => val,
101 Err(err) => Self::from_string_trunc(truncate_string(err, LenT::MAX.to_usize())),
102 }
103 }
104
105 #[must_use]
107 pub fn len(&self) -> LenT {
108 match &self.0 {
109 FixedStringRepr::Heap(a) => a.len(),
110 FixedStringRepr::Static(a) => a.len(),
111 FixedStringRepr::Inline(a) => a.len().into(),
112 }
113 }
114
115 #[must_use]
117 pub fn is_empty(&self) -> bool {
118 self.len() == LenT::ZERO
119 }
120
121 #[must_use]
123 pub fn as_str(&self) -> &str {
124 self
125 }
126
127 #[must_use]
129 pub fn into_string(self) -> String {
130 self.into()
131 }
132
133 #[cfg(test)]
134 #[must_use]
135 pub(crate) fn is_inline(&self) -> bool {
136 matches!(self, Self(FixedStringRepr::Inline(_)))
137 }
138
139 #[cfg(test)]
140 #[must_use]
141 pub(crate) fn is_static(&self) -> bool {
142 matches!(self, Self(FixedStringRepr::Static(_)))
143 }
144}
145
146impl<LenT: ValidLength> core::ops::Deref for FixedString<LenT> {
147 type Target = str;
148
149 fn deref(&self) -> &Self::Target {
150 match &self.0 {
151 FixedStringRepr::Heap(a) => unsafe { core::str::from_utf8_unchecked(a) },
153 FixedStringRepr::Static(a) => a.as_str(),
154 FixedStringRepr::Inline(a) => a.as_str(),
155 }
156 }
157}
158
159impl<LenT: ValidLength> Default for FixedString<LenT> {
160 fn default() -> Self {
161 FixedString::new()
162 }
163}
164
165impl<LenT: ValidLength> Clone for FixedString<LenT> {
166 fn clone(&self) -> Self {
167 match &self.0 {
168 FixedStringRepr::Heap(a) => Self(FixedStringRepr::Heap(a.clone())),
169 FixedStringRepr::Inline(a) => Self(FixedStringRepr::Inline(*a)),
170 FixedStringRepr::Static(a) => Self(FixedStringRepr::Static(*a)),
171 }
172 }
173
174 fn clone_from(&mut self, source: &Self) {
175 match (&mut self.0, &source.0) {
176 (FixedStringRepr::Heap(new), FixedStringRepr::Heap(src)) => new.clone_from(src),
177 #[allow(clippy::assigning_clones)]
178 _ => *self = source.clone(),
179 }
180 }
181}
182
183impl<LenT: ValidLength> Hash for FixedString<LenT> {
184 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
185 self.as_str().hash(state);
186 }
187}
188
189impl<LenT: ValidLength> PartialEq for FixedString<LenT> {
190 fn eq(&self, other: &Self) -> bool {
191 self.as_str() == other.as_str()
192 }
193}
194
195impl<LenT: ValidLength> Eq for FixedString<LenT> {}
196
197impl<LenT: ValidLength> PartialEq<String> for FixedString<LenT> {
198 fn eq(&self, other: &String) -> bool {
199 self.as_str().eq(other)
200 }
201}
202
203impl<LenT: ValidLength> PartialEq<&str> for FixedString<LenT> {
204 fn eq(&self, other: &&str) -> bool {
205 self.as_str().eq(*other)
206 }
207}
208
209impl<LenT: ValidLength> PartialEq<str> for FixedString<LenT> {
210 fn eq(&self, other: &str) -> bool {
211 self.as_str().eq(other)
212 }
213}
214
215impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for &str {
216 fn eq(&self, other: &FixedString<LenT>) -> bool {
217 other == self
218 }
219}
220
221impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for str {
222 fn eq(&self, other: &FixedString<LenT>) -> bool {
223 other == self
224 }
225}
226
227impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for String {
228 fn eq(&self, other: &FixedString<LenT>) -> bool {
229 other == self
230 }
231}
232
233impl<LenT: ValidLength> core::cmp::PartialOrd for FixedString<LenT> {
234 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
235 Some(self.cmp(other))
236 }
237}
238
239impl<LenT: ValidLength> core::cmp::Ord for FixedString<LenT> {
240 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
241 self.as_str().cmp(other.as_str())
242 }
243}
244
245impl<LenT: ValidLength> core::fmt::Display for FixedString<LenT> {
246 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247 f.write_str(self)
248 }
249}
250
251impl<LenT: ValidLength> core::fmt::Debug for FixedString<LenT> {
252 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253 write!(f, "{:?}", self.as_str())
254 }
255}
256
257impl<LenT: ValidLength> FromStr for FixedString<LenT> {
258 type Err = InvalidStrLength;
259
260 fn from_str(val: &str) -> Result<Self, Self::Err> {
261 if let Some(inline) = Self::new_inline(val) {
262 Ok(inline)
263 } else {
264 Self::try_from(Box::from(val))
265 }
266 }
267}
268
269impl<LenT: ValidLength> TryFrom<Box<str>> for FixedString<LenT> {
270 type Error = InvalidStrLength;
271
272 fn try_from(value: Box<str>) -> Result<Self, Self::Error> {
273 if let Some(inline) = Self::new_inline(&value) {
274 return Ok(inline);
275 }
276
277 match value.into_boxed_bytes().try_into() {
278 Ok(val) => Ok(Self(FixedStringRepr::Heap(val))),
279 Err(err) => Err(err
280 .try_into()
281 .expect("Box<str> -> Box<[u8]> should stay valid UTF8")),
282 }
283 }
284}
285
286impl<LenT: ValidLength> TryFrom<String> for FixedString<LenT> {
287 type Error = InvalidStrLength;
288
289 fn try_from(value: String) -> Result<Self, Self::Error> {
290 if let Some(inline) = Self::new_inline(&value) {
291 return Ok(inline);
292 }
293
294 value.into_boxed_str().try_into()
295 }
296}
297
298impl<LenT: ValidLength> From<char> for FixedString<LenT> {
299 fn from(value: char) -> Self {
300 use alloc::vec;
301
302 if let Some(value) = InlineString::from_char(value) {
303 return Self(FixedStringRepr::Inline(value));
304 }
305
306 let mut bytes = vec![0; value.len_utf8()].into_boxed_slice();
307
308 value.encode_utf8(&mut bytes);
309
310 let bytes = bytes
311 .try_into()
312 .expect("len_utf8 is at most 4, so it will fit in u8");
313
314 Self(FixedStringRepr::Heap(bytes))
315 }
316}
317
318impl<LenT: ValidLength> From<FixedString<LenT>> for String {
319 fn from(value: FixedString<LenT>) -> Self {
320 Box::<str>::from(value).into()
321 }
322}
323
324impl<LenT: ValidLength> From<FixedString<LenT>> for Box<str> {
325 fn from(value: FixedString<LenT>) -> Self {
326 match value.0 {
327 FixedStringRepr::Inline(a) => a.as_str().into(),
328 FixedStringRepr::Static(a) => a.as_str().into(),
329 FixedStringRepr::Heap(a) => unsafe { alloc::str::from_boxed_utf8_unchecked(a.into()) },
331 }
332 }
333}
334
335impl<'a, LenT: ValidLength> From<&'a FixedString<LenT>> for Cow<'a, str> {
336 fn from(value: &'a FixedString<LenT>) -> Self {
337 Cow::Borrowed(value.as_str())
338 }
339}
340
341impl<LenT: ValidLength> From<FixedString<LenT>> for Cow<'_, str> {
342 fn from(value: FixedString<LenT>) -> Self {
343 match value.0 {
344 FixedStringRepr::Static(static_str) => Cow::Borrowed(static_str.as_str()),
345 _ => Cow::Owned(value.into()),
346 }
347 }
348}
349
350impl<LenT: ValidLength> AsRef<str> for FixedString<LenT> {
351 fn as_ref(&self) -> &str {
352 self
353 }
354}
355
356impl<LenT: ValidLength> Borrow<str> for FixedString<LenT> {
357 fn borrow(&self) -> &str {
358 self
359 }
360}
361
362#[cfg(feature = "std")]
363impl<LenT: ValidLength> AsRef<std::path::Path> for FixedString<LenT> {
364 fn as_ref(&self) -> &std::path::Path {
365 self.as_str().as_ref()
366 }
367}
368
369#[cfg(feature = "std")]
370impl<LenT: ValidLength> AsRef<std::ffi::OsStr> for FixedString<LenT> {
371 fn as_ref(&self) -> &std::ffi::OsStr {
372 self.as_str().as_ref()
373 }
374}
375
376impl<LenT: ValidLength> From<FixedString<LenT>> for Arc<str> {
377 fn from(value: FixedString<LenT>) -> Self {
378 Arc::from(value.into_string())
379 }
380}
381
382#[cfg(feature = "to-arraystring")]
383impl to_arraystring::ToArrayString for &FixedString<u8> {
384 const MAX_LENGTH: usize = 255;
385 type ArrayString = to_arraystring::ArrayString<255>;
386
387 fn to_arraystring(self) -> Self::ArrayString {
388 Self::ArrayString::from(self).unwrap()
389 }
390}
391
392#[cfg(feature = "serde")]
393impl<'de, LenT: ValidLength> serde::Deserialize<'de> for FixedString<LenT> {
394 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
395 use core::marker::PhantomData;
396
397 struct Visitor<LenT: ValidLength>(PhantomData<LenT>);
398
399 impl<LenT: ValidLength> serde::de::Visitor<'_> for Visitor<LenT> {
400 type Value = FixedString<LenT>;
401
402 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
403 write!(formatter, "a string up to {} bytes long", LenT::MAX)
404 }
405
406 fn visit_str<E: serde::de::Error>(self, val: &str) -> Result<Self::Value, E> {
407 FixedString::from_str(val).map_err(E::custom)
408 }
409
410 fn visit_string<E: serde::de::Error>(self, val: String) -> Result<Self::Value, E> {
411 FixedString::try_from(val.into_boxed_str()).map_err(E::custom)
412 }
413 }
414
415 deserializer.deserialize_string(Visitor(PhantomData))
416 }
417}
418
419#[cfg(feature = "serde")]
420impl<LenT: ValidLength> serde::Serialize for FixedString<LenT> {
421 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
422 self.as_str().serialize(serializer)
423 }
424}
425
426#[cfg(test)]
427mod test {
428 use super::*;
429
430 fn check_u8_roundtrip_generic(to_fixed: fn(String) -> FixedString<u8>) {
431 for i in 0..=u8::MAX {
432 let original = "a".repeat(i.into());
433 let fixed = to_fixed(original);
434
435 assert!(fixed.bytes().all(|c| c == b'a'));
436 assert_eq!(fixed.len(), i);
437
438 if !fixed.is_static() {
439 assert_eq!(fixed.is_inline(), fixed.len() <= 9);
440 }
441 }
442 }
443
444 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
446 fn check_u32_partial_roundtrip_generic(to_fixed: fn(String) -> FixedString<u32>) {
447 for i in 0..=400u32 {
448 let original = "a".repeat(i.try_into().expect("should be less than usize::MAX"));
449 let fixed = to_fixed(original);
450
451 assert!(fixed.bytes().all(|c| c == b'a'));
452 assert_eq!(fixed.len(), i);
453
454 if !fixed.is_static() {
455 assert_eq!(fixed.is_inline(), fixed.len() <= 12);
456 }
457 }
458 }
459
460 fn check_default_generic<LenT: ValidLength>() {
461 let fixed = FixedString::<LenT>::default();
462
463 assert!(fixed.is_static());
464 assert_eq!(fixed.as_str(), "");
465 }
466
467 #[test]
468 fn test_truncating_behaviour() {
469 const STR: &str = "______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________🦀";
470
471 let string = FixedString::<u8>::from_static_trunc(STR);
472
473 let str = std::str::from_utf8(string.as_bytes()).expect("is utf8");
474
475 assert_eq!(str, string.as_str());
476 assert_ne!(STR, str);
477 }
478
479 #[test]
480 fn test_from_static_to_cow() {
481 const STR: &str = "static string";
482
483 let string = FixedString::<u8>::from_static_trunc(STR);
484
485 let cow: std::borrow::Cow<'static, _> = string.into();
486
487 assert_eq!(cow, STR);
488
489 let std::borrow::Cow::Borrowed(string) = cow else {
490 panic!("Expected borrowed string");
491 };
492
493 assert_eq!(string, STR);
494 }
495
496 #[test]
497 fn check_u8_roundtrip() {
498 check_u8_roundtrip_generic(|original| {
499 FixedString::<u8>::try_from(original.into_boxed_str()).unwrap()
500 });
501 }
502
503 #[test]
504 fn check_u8_roundtrip_static() {
505 check_u8_roundtrip_generic(|original| {
506 let static_str = Box::leak(original.into_boxed_str());
507 FixedString::from_static_trunc(static_str)
508 });
509 }
510
511 #[test]
512 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
513 fn check_u32_partial_roundtrip() {
514 check_u32_partial_roundtrip_generic(|original| {
515 FixedString::<u32>::try_from(original).unwrap()
516 });
517 }
518
519 #[test]
520 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
521 fn check_u32_partial_roundtrip_static() {
522 check_u32_partial_roundtrip_generic(|original| {
523 let static_str = Box::leak(original.into_boxed_str());
524 FixedString::from_static_trunc(static_str)
525 });
526 }
527
528 #[test]
529 #[cfg(feature = "serde")]
530 fn check_u8_roundtrip_serde() {
531 check_u8_roundtrip_generic(|original| {
532 serde_json::from_str(&alloc::format!("\"{original}\"")).unwrap()
533 });
534 }
535
536 #[test]
537 #[cfg(feature = "to-arraystring")]
538 fn check_u8_roundtrip_arraystring() {
539 use to_arraystring::ToArrayString;
540
541 check_u8_roundtrip_generic(|original| {
542 FixedString::from_str_trunc(
543 FixedString::from_string_trunc(original)
544 .to_arraystring()
545 .as_str(),
546 )
547 });
548 }
549
550 #[test]
551 fn check_default_u8() {
552 check_default_generic::<u8>();
553 }
554
555 #[test]
556 fn check_default_u16() {
557 check_default_generic::<u16>();
558 }
559
560 #[test]
561 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
562 fn check_default_u32() {
563 check_default_generic::<u32>();
564 }
565
566 #[test]
567 fn check_sizes() {
568 type DoubleOpt<T> = Option<Option<T>>;
569
570 assert_eq!(core::mem::size_of::<Option<InlineString<[u8; 11]>>>(), 12);
571 assert_eq!(core::mem::align_of::<Option<InlineString<[u8; 11]>>>(), 1);
572 assert_eq!(core::mem::size_of::<Option<FixedArray<u8, u32>>>(), 12);
573 assert_eq!(core::mem::size_of::<DoubleOpt<FixedArray<u8, u32>>>(), 13);
575 assert_eq!(core::mem::align_of::<Option<FixedArray<u8, u32>>>(), 1);
576 assert_eq!(core::mem::size_of::<FixedStringRepr<u32>>(), 13);
578 assert_eq!(core::mem::align_of::<FixedStringRepr<u32>>(), 1);
579 }
580
581 #[test]
582 fn from_char_u8() {
583 let s: FixedString<u8> = 'a'.into();
584 assert_eq!(s.len(), 1);
585 assert!(s.is_inline());
586
587 let s: FixedString<u8> = '¼'.into();
588 assert_eq!(s.len(), 2);
589 assert!(s.is_inline());
590
591 let s: FixedString<u8> = 'âš¡'.into();
592 assert_eq!(s.len(), 3);
593 assert!(s.is_inline());
594
595 let s: FixedString<u8> = '🦀'.into();
596 assert_eq!(s.len(), 4);
597 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
598 assert!(s.is_inline());
599 }
600
601 #[test]
602 fn from_char_u16() {
603 let s: FixedString<u16> = 'a'.into();
604 assert_eq!(s.len(), 1);
605 assert!(s.is_inline());
606
607 let s: FixedString<u16> = '¼'.into();
608 assert_eq!(s.len(), 2);
609 assert!(s.is_inline());
610
611 let s: FixedString<u16> = 'âš¡'.into();
612 assert_eq!(s.len(), 3);
613 assert!(s.is_inline());
614
615 let s: FixedString<u16> = '🦀'.into();
616 assert_eq!(s.len(), 4);
617 assert!(s.is_inline());
618 }
619
620 #[test]
621 #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
622 fn from_char_u32() {
623 let s: FixedString<u32> = 'a'.into();
624 assert_eq!(s.len(), 1);
625 assert!(s.is_inline());
626
627 let s: FixedString<u32> = '¼'.into();
628 assert_eq!(s.len(), 2);
629 assert!(s.is_inline());
630
631 let s: FixedString<u32> = 'âš¡'.into();
632 assert_eq!(s.len(), 3);
633 assert!(s.is_inline());
634
635 let s: FixedString<u32> = '🦀'.into();
636 assert_eq!(s.len(), 4);
637 assert!(s.is_inline());
638 }
639}