Skip to main content

asciidoc_parser/
strings.rs

1// Adapted from pulldown-cmark, which comes with the following license:
2//
3// Copyright 2015 Google Inc. All rights reserved.
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21// THE SOFTWARE.
22
23//! String types that facilitate parsing.
24
25use std::{
26    borrow::{Borrow, Cow},
27    fmt,
28    hash::{Hash, Hasher},
29    ops::Deref,
30    str::from_utf8,
31};
32
33pub(crate) const MAX_INLINE_STR_LEN: usize = 3 * std::mem::size_of::<isize>() - 2;
34
35/// Returned when trying to convert a `&str` into an [`InlineStr`] but it fails
36/// because it doesn't fit.
37#[derive(Debug)]
38pub struct StringTooLongError;
39
40/// An inline string that can contain almost three words
41/// of UTF-8 text.
42#[derive(Debug, Clone, Copy, Eq)]
43pub struct InlineStr {
44    inner: [u8; MAX_INLINE_STR_LEN],
45    len: u8,
46}
47
48impl AsRef<str> for InlineStr {
49    fn as_ref(&self) -> &str {
50        self.deref()
51    }
52}
53
54impl Hash for InlineStr {
55    fn hash<H: Hasher>(&self, state: &mut H) {
56        self.deref().hash(state);
57    }
58}
59
60impl From<char> for InlineStr {
61    fn from(c: char) -> Self {
62        let mut inner = [0u8; MAX_INLINE_STR_LEN];
63        c.encode_utf8(&mut inner);
64        let len = c.len_utf8() as u8;
65        Self { inner, len }
66    }
67}
68
69impl std::cmp::PartialEq<InlineStr> for InlineStr {
70    fn eq(&self, other: &InlineStr) -> bool {
71        self.deref() == other.deref()
72    }
73}
74
75impl TryFrom<&str> for InlineStr {
76    type Error = StringTooLongError;
77
78    fn try_from(s: &str) -> Result<InlineStr, StringTooLongError> {
79        let len = s.len();
80        if len <= MAX_INLINE_STR_LEN {
81            let mut inner = [0u8; MAX_INLINE_STR_LEN];
82
83            debug_assert!(
84                len <= MAX_INLINE_STR_LEN,
85                "InlineStr: len {} exceeds MAX_INLINE_STR_LEN {}",
86                len,
87                MAX_INLINE_STR_LEN
88            );
89
90            if let Some(dest) = inner.get_mut(..len) {
91                dest.copy_from_slice(s.as_bytes());
92            }
93
94            let len = len as u8;
95            Ok(Self { inner, len })
96        } else {
97            Err(StringTooLongError)
98        }
99    }
100}
101
102impl Deref for InlineStr {
103    type Target = str;
104
105    fn deref(&self) -> &str {
106        let len = self.len as usize;
107        self.inner
108            .get(..len)
109            .and_then(|bytes| from_utf8(bytes).ok())
110            .unwrap_or_default()
111    }
112}
113
114impl fmt::Display for InlineStr {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.as_ref())
117    }
118}
119
120/// A copy-on-write string that can be owned, borrowed,
121/// or inlined.
122///
123/// It is three words long.
124///
125/// NOTE: The [`Debug`] implementation for this struct elides the storage
126/// mechanism that is chosen when pretty printing (as occurs when using the
127/// `dbg!()` macro. To obtain that information, use the β€œnormal” debug
128/// formatting as shown below:
129///
130/// ```
131/// # use asciidoc_parser::strings::CowStr;
132///
133/// let s: &'static str = "0123456789abcdefghijklm";
134/// let s: CowStr = s.into();
135/// assert_eq!(
136///     format!("{s:?}"),
137///     "CowStr::Borrowed(\"0123456789abcdefghijklm\")"
138/// );
139/// ```
140#[derive(Eq)]
141pub enum CowStr<'a> {
142    /// An owned, immutable string.
143    Boxed(Box<str>),
144
145    /// A borrowed string.
146    Borrowed(&'a str),
147
148    /// A short inline string.
149    Inlined(InlineStr),
150}
151
152impl AsRef<str> for CowStr<'_> {
153    fn as_ref(&self) -> &str {
154        self.deref()
155    }
156}
157
158impl Hash for CowStr<'_> {
159    fn hash<H: Hasher>(&self, state: &mut H) {
160        self.deref().hash(state);
161    }
162}
163
164impl std::clone::Clone for CowStr<'_> {
165    fn clone(&self) -> Self {
166        match self {
167            CowStr::Boxed(s) => match InlineStr::try_from(&**s) {
168                Ok(inline) => CowStr::Inlined(inline),
169                Err(..) => CowStr::Boxed(s.clone()),
170            },
171            CowStr::Borrowed(s) => CowStr::Borrowed(s),
172            CowStr::Inlined(s) => CowStr::Inlined(*s),
173        }
174    }
175}
176
177impl<'a> std::cmp::PartialEq<CowStr<'a>> for CowStr<'a> {
178    fn eq(&self, other: &CowStr<'_>) -> bool {
179        self.deref() == other.deref()
180    }
181}
182
183impl<'a> From<&'a str> for CowStr<'a> {
184    fn from(s: &'a str) -> Self {
185        CowStr::Borrowed(s)
186    }
187}
188
189impl From<String> for CowStr<'_> {
190    fn from(s: String) -> Self {
191        CowStr::Boxed(s.into_boxed_str())
192    }
193}
194
195impl From<char> for CowStr<'_> {
196    fn from(c: char) -> Self {
197        CowStr::Inlined(c.into())
198    }
199}
200
201impl<'a> From<Cow<'a, str>> for CowStr<'a> {
202    fn from(s: Cow<'a, str>) -> Self {
203        match s {
204            Cow::Borrowed(s) => CowStr::Borrowed(s),
205            Cow::Owned(s) => CowStr::Boxed(s.into_boxed_str()),
206        }
207    }
208}
209
210impl<'a> From<CowStr<'a>> for Cow<'a, str> {
211    fn from(s: CowStr<'a>) -> Self {
212        match s {
213            CowStr::Boxed(s) => Cow::Owned(s.to_string()),
214            CowStr::Inlined(s) => Cow::Owned(s.to_string()),
215            CowStr::Borrowed(s) => Cow::Borrowed(s),
216        }
217    }
218}
219
220impl<'a> From<Cow<'a, char>> for CowStr<'a> {
221    fn from(s: Cow<'a, char>) -> Self {
222        CowStr::Inlined(InlineStr::from(*s))
223    }
224}
225
226impl Deref for CowStr<'_> {
227    type Target = str;
228
229    fn deref(&self) -> &str {
230        match self {
231            CowStr::Boxed(b) => b,
232            CowStr::Borrowed(b) => b,
233            CowStr::Inlined(s) => s.deref(),
234        }
235    }
236}
237
238impl Borrow<str> for CowStr<'_> {
239    fn borrow(&self) -> &str {
240        self.deref()
241    }
242}
243
244impl CowStr<'_> {
245    /// Convert the `CowStr` into an owned `String`.
246    pub fn into_string(self) -> String {
247        match self {
248            CowStr::Boxed(b) => b.into(),
249            CowStr::Borrowed(b) => b.to_owned(),
250            CowStr::Inlined(s) => s.deref().to_owned(),
251        }
252    }
253}
254
255impl fmt::Display for CowStr<'_> {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "{}", self.as_ref())
258    }
259}
260
261impl fmt::Debug for CowStr<'_> {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        if f.alternate() {
264            write!(f, "{:?}", self.as_ref())
265        } else {
266            match self {
267                Self::Boxed(b) => f.debug_tuple("CowStr::Boxed").field(b).finish(),
268                Self::Borrowed(b) => f.debug_tuple("CowStr::Borrowed").field(b).finish(),
269                Self::Inlined(s) => f.debug_tuple("CowStr::Inlined").field(s).finish(),
270            }
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    #![allow(clippy::panic)]
278    #![allow(clippy::unwrap_used)]
279
280    mod string_too_long_err {
281        use crate::strings::StringTooLongError;
282
283        #[test]
284        fn impl_debug() {
285            let e = StringTooLongError;
286            assert_eq!(format!("{e:#?}"), "StringTooLongError");
287        }
288    }
289
290    mod inline_str {
291        use std::ops::Deref;
292
293        use crate::strings::*;
294
295        #[test]
296        fn from_ascii() {
297            let s: InlineStr = 'a'.into();
298            assert_eq!("a", s.deref());
299        }
300
301        #[test]
302        fn from_unicode() {
303            let s: InlineStr = 'πŸ”'.into();
304            assert_eq!("πŸ”", s.deref());
305        }
306
307        #[test]
308        fn impl_debug() {
309            let s: InlineStr = 'a'.into();
310            assert_eq!(
311                format!("{s:#?}"),
312                r#"InlineStr {
313    inner: [
314        97,
315        0,
316        0,
317        0,
318        0,
319        0,
320        0,
321        0,
322        0,
323        0,
324        0,
325        0,
326        0,
327        0,
328        0,
329        0,
330        0,
331        0,
332        0,
333        0,
334        0,
335        0,
336    ],
337    len: 1,
338}"#
339            );
340        }
341
342        #[test]
343        fn impl_hash() {
344            use std::{
345                collections::hash_map::DefaultHasher,
346                hash::{Hash, Hasher},
347            };
348
349            let mut hasher = DefaultHasher::new();
350            "πŸ”".hash(&mut hasher);
351            let expected = hasher.finish();
352
353            let s: InlineStr = 'πŸ”'.into();
354            let mut hasher = DefaultHasher::new();
355            s.hash(&mut hasher);
356            let actual = hasher.finish();
357
358            let s: InlineStr = 'a'.into();
359            let mut hasher = DefaultHasher::new();
360            s.hash(&mut hasher);
361            let mismatch = hasher.finish();
362
363            assert_eq!(expected, actual);
364            assert_ne!(expected, mismatch);
365        }
366
367        #[test]
368        fn impl_partial_eq() {
369            let s1: InlineStr = 'πŸ”'.into();
370            let s2: InlineStr = 'πŸ”'.into();
371            let s3: InlineStr = 'a'.into();
372
373            assert_eq!(s1, s2);
374            assert_ne!(s1, s3);
375        }
376
377        #[test]
378        #[allow(clippy::assertions_on_constants)]
379        fn max_len_atleast_four() {
380            // We need 4 bytes to store a char.
381            assert!(MAX_INLINE_STR_LEN >= 4);
382        }
383
384        #[test]
385        #[cfg(target_pointer_width = "64")]
386        fn fits_twentytwo() {
387            let s = "0123456789abcdefghijkl";
388            let stack_str = InlineStr::try_from(s).unwrap();
389            assert_eq!(stack_str.deref(), s);
390        }
391
392        #[test]
393        #[cfg(target_pointer_width = "64")]
394        fn doesnt_fit_twentythree() {
395            let s = "0123456789abcdefghijklm";
396            let _stack_str = InlineStr::try_from(s).unwrap_err();
397        }
398    }
399
400    mod cow_str {
401        use std::{
402            borrow::{Borrow, Cow},
403            ops::Deref,
404        };
405
406        use crate::strings::*;
407
408        #[test]
409        fn size() {
410            let size = std::mem::size_of::<CowStr>();
411            let word_size = std::mem::size_of::<isize>();
412            assert_eq!(3 * word_size, size);
413        }
414
415        #[test]
416        fn char_to_string() {
417            let c = '藏';
418            let smort: CowStr = c.into();
419            let owned: String = smort.to_string();
420            let expected = "藏".to_owned();
421            assert_eq!(expected, owned);
422        }
423
424        #[test]
425        #[cfg(target_pointer_width = "64")]
426        fn small_boxed_str_clones_to_stack() {
427            let s = "0123456789abcde".to_owned();
428            let smort: CowStr = s.into();
429            let smort_clone = smort.clone();
430
431            if let CowStr::Inlined(..) = smort_clone {
432            } else {
433                panic!("Expected a Inlined variant!");
434            }
435        }
436
437        #[test]
438        fn cow_to_cow_str() {
439            let s = "some text";
440            let cow = Cow::Borrowed(s);
441            let actual = CowStr::from(cow);
442            let expected = CowStr::Borrowed(s);
443            assert_eq!(actual, expected);
444            assert!(variant_eq(&actual, &expected));
445
446            let s = "some text".to_string();
447            let cow: Cow<str> = Cow::Owned(s.clone());
448            let actual = CowStr::from(cow);
449            let expected = CowStr::Boxed(s.into_boxed_str());
450            assert_eq!(actual, expected);
451            assert!(variant_eq(&actual, &expected));
452        }
453
454        #[test]
455        fn cow_str_to_cow() {
456            let s = "some text";
457            let cow_str = CowStr::Borrowed(s);
458            let actual = Cow::from(cow_str);
459            let expected = Cow::Borrowed(s);
460            assert_eq!(actual, expected);
461            assert!(variant_eq(&actual, &expected));
462
463            let s = "s";
464            let inline_str: InlineStr = InlineStr::try_from(s).unwrap();
465            let cow_str = CowStr::Inlined(inline_str);
466            let actual = Cow::from(cow_str);
467            let expected: Cow<str> = Cow::Owned(s.to_string());
468            assert_eq!(actual, expected);
469            assert!(variant_eq(&actual, &expected));
470
471            let s = "s";
472            let cow_str = CowStr::Boxed(s.to_string().into_boxed_str());
473            let actual = Cow::from(cow_str);
474            let expected: Cow<str> = Cow::Owned(s.to_string());
475            assert_eq!(actual, expected);
476            assert!(variant_eq(&actual, &expected));
477        }
478
479        #[test]
480        fn cow_char_to_cow_str() {
481            let c = 'c';
482            let cow: Cow<char> = Cow::Owned(c);
483            let actual = CowStr::from(cow);
484            let expected = CowStr::Inlined(InlineStr::from(c));
485            assert_eq!(actual, expected);
486            assert!(variant_eq(&actual, &expected));
487
488            let c = 'c';
489            let cow: Cow<char> = Cow::Borrowed(&c);
490            let actual = CowStr::from(cow);
491            let expected = CowStr::Inlined(InlineStr::from(c));
492            assert_eq!(actual, expected);
493            assert!(variant_eq(&actual, &expected));
494        }
495
496        fn variant_eq<T>(a: &T, b: &T) -> bool {
497            std::mem::discriminant(a) == std::mem::discriminant(b)
498        }
499
500        #[test]
501        fn impl_debug_pretty_print_for_inline() {
502            let c = '藏';
503            let s: CowStr = c.into();
504
505            assert_eq!(format!("{s:#?}"), r#""藏""#);
506        }
507
508        #[test]
509        fn impl_debug_pretty_print_for_boxed() {
510            let s = "blah blah blah".to_string();
511            let s: CowStr = s.into();
512
513            assert_eq!(format!("{s:#?}"), r#""blah blah blah""#);
514        }
515
516        #[test]
517        fn impl_debug_pretty_print_for_borrowed() {
518            let s: &'static str = "0123456789abcdefghijklm";
519            let s: CowStr = s.into();
520
521            assert_eq!(format!("{s:#?}"), r#""0123456789abcdefghijklm""#);
522        }
523
524        #[test]
525        fn impl_debug_for_inline() {
526            let c = '藏';
527            let s: CowStr = c.into();
528
529            assert_eq!(
530                format!("{s:?}"),
531                "CowStr::Inlined(InlineStr { inner: [232, 151, 143, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 3 })"
532            );
533        }
534
535        #[test]
536        fn impl_debug_for_boxed() {
537            let s = "blah blah blah".to_string();
538            let s: CowStr = s.into();
539
540            assert_eq!(format!("{s:?}"), "CowStr::Boxed(\"blah blah blah\")");
541        }
542
543        #[test]
544        fn impl_debug_for_borrowed() {
545            let s: &'static str = "0123456789abcdefghijklm";
546            let s: CowStr = s.into();
547
548            assert_eq!(
549                format!("{s:?}"),
550                "CowStr::Borrowed(\"0123456789abcdefghijklm\")"
551            );
552        }
553
554        #[test]
555        fn impl_clone_boxed_long() {
556            let s = "this string won't fit in a box".to_owned();
557            let s: CowStr = s.into();
558            if let CowStr::Boxed(_) = s {
559            } else {
560                panic!("Expected Boxed case");
561            }
562
563            let s2 = s.clone();
564            assert_eq!(s.deref(), s2.deref());
565
566            if let CowStr::Boxed(_) = s2 {
567            } else {
568                panic!("Expected Boxed clone");
569            }
570        }
571
572        #[test]
573        fn impl_clone_borrowed() {
574            let s = "this long string is borrowed";
575            let s: CowStr = s.into();
576            if let CowStr::Borrowed(_) = s {
577            } else {
578                panic!("Expected Borrowed case");
579            }
580
581            let s2 = s.clone();
582            assert_eq!(s.deref(), s2.deref());
583
584            if let CowStr::Borrowed(_) = s2 {
585            } else {
586                panic!("Expected Borrowed clone");
587            }
588        }
589
590        #[test]
591        fn impl_clone_inlined() {
592            let s: CowStr = 's'.into();
593            if let CowStr::Inlined(_) = s {
594            } else {
595                panic!("Expected Inlined case");
596            }
597
598            let s2 = s.clone();
599            assert_eq!(s.deref(), s2.deref());
600
601            if let CowStr::Inlined(_) = s2 {
602            } else {
603                panic!("Expected Inlined clone");
604            }
605        }
606
607        #[test]
608        fn impl_hash() {
609            use std::{
610                collections::hash_map::DefaultHasher,
611                hash::{Hash, Hasher},
612            };
613
614            let mut hasher = DefaultHasher::new();
615            "πŸ”".hash(&mut hasher);
616            let expected = hasher.finish();
617
618            let s: CowStr = 'πŸ”'.into();
619            if let CowStr::Inlined(_) = s {
620            } else {
621                panic!("Expected Inlined case");
622            }
623            let mut hasher = DefaultHasher::new();
624            s.hash(&mut hasher);
625            let actual = hasher.finish();
626            assert_eq!(expected, actual);
627
628            let s = CowStr::Borrowed("πŸ”");
629            let mut hasher = DefaultHasher::new();
630            s.hash(&mut hasher);
631            let actual = hasher.finish();
632            assert_eq!(expected, actual);
633
634            let s = "πŸ”".to_owned();
635            let s: CowStr = s.into();
636            if let CowStr::Boxed(_) = s {
637            } else {
638                panic!("Expected Boxed case");
639            }
640            let mut hasher = DefaultHasher::new();
641            s.hash(&mut hasher);
642            assert_eq!(expected, actual);
643        }
644
645        #[test]
646        fn impl_from_str() {
647            let s = "xyz";
648            let s: CowStr = s.into();
649            assert_eq!(s.deref(), "xyz");
650
651            if let CowStr::Borrowed(_) = s {
652            } else {
653                panic!("Expected Borrowed case");
654            }
655        }
656
657        #[test]
658        fn impl_borrow() {
659            let s: CowStr = "xyz".into();
660            let s: &str = s.borrow();
661            assert_eq!(s, "xyz");
662        }
663
664        #[test]
665        fn into_string_boxed() {
666            let s = "this string won't fit in a box".to_owned();
667            let s: CowStr = s.into();
668            if let CowStr::Boxed(_) = s {
669            } else {
670                panic!("Expected Boxed case");
671            }
672
673            let s2 = s.into_string();
674            assert_eq!(&s2, "this string won't fit in a box");
675        }
676
677        #[test]
678        fn into_string_borrowed() {
679            let s = "this long string is borrowed";
680            let s: CowStr = s.into();
681            if let CowStr::Borrowed(_) = s {
682            } else {
683                panic!("Expected Borrowed case");
684            }
685
686            let s2 = s.into_string();
687            assert_eq!(&s2, "this long string is borrowed");
688        }
689
690        #[test]
691        fn into_string_inlined() {
692            let s: CowStr = 's'.into();
693            if let CowStr::Inlined(_) = s {
694            } else {
695                panic!("Expected Inlined case");
696            }
697
698            let s2 = s.into_string();
699            assert_eq!(&s2, "s");
700        }
701    }
702}