Skip to main content

swc_atoms/
wtf8_atom.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Formatter},
4    ops::Deref,
5};
6
7use hstr::wtf8::{Wtf8, Wtf8Buf};
8use serde::Serializer;
9
10use crate::Atom;
11
12/// Clone-on-write WTF-8 string.
13///
14///
15/// See [tendril] for more details.
16#[derive(Clone, Default, PartialEq, Eq, Hash)]
17#[repr(transparent)]
18pub struct Wtf8Atom(pub(super) hstr::Wtf8Atom);
19
20#[cfg(feature = "encoding-impl")]
21impl cbor4ii::core::enc::Encode for Wtf8Atom {
22    #[inline]
23    fn encode<W: cbor4ii::core::enc::Write>(
24        &self,
25        writer: &mut W,
26    ) -> Result<(), cbor4ii::core::enc::Error<W::Error>> {
27        cbor4ii::core::types::Bytes(self.as_bytes()).encode(writer)
28    }
29}
30
31#[cfg(feature = "encoding-impl")]
32impl<'de> cbor4ii::core::dec::Decode<'de> for Wtf8Atom {
33    #[inline]
34    fn decode<R: cbor4ii::core::dec::Read<'de>>(
35        reader: &mut R,
36    ) -> Result<Self, cbor4ii::core::dec::Error<R::Error>> {
37        let s = <cbor4ii::core::types::Bytes<&[u8]>>::decode(reader)?;
38
39        // This is not sound, maybe Wtf8Buf should make bytes operations safe
40        Ok(Self(hstr::Wtf8Atom::from(unsafe {
41            Wtf8Buf::from_bytes_unchecked(s.0.into())
42        })))
43    }
44}
45
46#[cfg(feature = "arbitrary")]
47#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
48impl<'a> arbitrary::Arbitrary<'a> for Wtf8Atom {
49    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
50        let sym = u.arbitrary::<Vec<u8>>()?;
51        if sym.is_empty() {
52            return Err(arbitrary::Error::NotEnoughData);
53        }
54        Ok(Self(hstr::Wtf8Atom::from(unsafe {
55            Wtf8Buf::from_bytes_unchecked(sym)
56        })))
57    }
58}
59
60fn _asserts() {
61    // let _static_assert_size_eq = std::mem::transmute::<Atom, [usize; 1]>;
62
63    fn _assert_send<T: Send>() {}
64    fn _assert_sync<T: Sync>() {}
65
66    _assert_sync::<Wtf8Atom>();
67    _assert_send::<Wtf8Atom>();
68}
69
70impl Wtf8Atom {
71    /// Creates a new [Wtf8Atom] from a string.
72    #[inline(always)]
73    pub fn new<S>(s: S) -> Self
74    where
75        hstr::Wtf8Atom: From<S>,
76    {
77        Wtf8Atom(hstr::Wtf8Atom::from(s))
78    }
79
80    pub fn as_wtf8(&self) -> &Wtf8 {
81        &self.0
82    }
83
84    pub fn as_atom(&self) -> Option<&Atom> {
85        if self.as_str().is_some() {
86            Some(unsafe { &*(self as *const Wtf8Atom as *const Atom) })
87        } else {
88            None
89        }
90    }
91
92    /// Returns the UTF-8 [`Atom`] representation, borrowing when possible.
93    pub fn to_atom_lossy(&self) -> Cow<'_, Atom> {
94        if let Some(atom) = self.as_atom() {
95            return Cow::Borrowed(atom);
96        }
97        Cow::Owned(Atom::new(self.to_string_lossy()))
98    }
99
100    /// Try to convert this to a UTF-8 [Atom].
101    ///
102    /// Returns [Atom] if the string is valid UTF-8, otherwise returns
103    /// the original [Wtf8Atom].
104    pub fn try_into_atom(self) -> Result<Atom, Wtf8Atom> {
105        self.0.try_into_atom().map(Atom).map_err(Wtf8Atom)
106    }
107
108    /// Creates a new [Wtf8Atom] from a byte slice.
109    ///
110    /// # Safety
111    ///
112    /// The caller must ensure that `bytes` is a well-formed WTF-8 byte
113    /// sequence.
114    ///
115    /// See [hstr::wtf8::Wtf8::from_bytes_unchecked] for more details.
116    pub unsafe fn from_bytes_unchecked(bytes: &[u8]) -> Self {
117        Wtf8Atom(hstr::Wtf8Atom::from(
118            hstr::wtf8::Wtf8::from_bytes_unchecked(bytes),
119        ))
120    }
121}
122
123impl Deref for Wtf8Atom {
124    type Target = Wtf8;
125
126    #[inline]
127    fn deref(&self) -> &Self::Target {
128        &self.0
129    }
130}
131
132impl fmt::Debug for Wtf8Atom {
133    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
134        fmt::Debug::fmt(&**self, f)
135    }
136}
137
138impl PartialOrd for Wtf8Atom {
139    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
140        Some(self.cmp(other))
141    }
142}
143
144impl Ord for Wtf8Atom {
145    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
146        self.as_wtf8().cmp(other.as_wtf8())
147    }
148}
149
150impl<T> PartialEq<T> for Wtf8Atom
151where
152    hstr::Wtf8Atom: PartialEq<T>,
153    T: ?Sized,
154{
155    fn eq(&self, other: &T) -> bool {
156        self.0.eq(other)
157    }
158}
159
160impl<T> From<T> for Wtf8Atom
161where
162    hstr::Wtf8Atom: From<T>,
163{
164    fn from(s: T) -> Self {
165        Wtf8Atom::new(s)
166    }
167}
168
169impl From<&Wtf8Atom> for Wtf8Buf {
170    fn from(s: &Wtf8Atom) -> Self {
171        // SAFETY: `Wtf8Atom` is guaranteed to be valid WTF-8 byte sequence.
172        unsafe { Wtf8Buf::from_bytes_unchecked(s.as_bytes().to_vec()) }
173    }
174}
175
176impl serde::ser::Serialize for Wtf8Atom {
177    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178    where
179        S: Serializer,
180    {
181        self.0.serialize(serializer)
182    }
183}
184
185impl<'de> serde::de::Deserialize<'de> for Wtf8Atom {
186    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187    where
188        D: serde::Deserializer<'de>,
189    {
190        hstr::Wtf8Atom::deserialize(deserializer).map(Wtf8Atom)
191    }
192}
193
194/// noop
195#[cfg(feature = "shrink-to-fit")]
196impl shrink_to_fit::ShrinkToFit for Wtf8Atom {
197    #[inline(always)]
198    fn shrink_to_fit(&mut self) {}
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn to_atom_lossy_returns_borrowed_for_utf8() {
207        let atom = Atom::from("swc");
208        let wtf = Wtf8Atom::from(atom.clone());
209
210        match wtf.to_atom_lossy() {
211            Cow::Borrowed(borrowed) => assert_eq!(borrowed, &atom),
212            Cow::Owned(_) => panic!("expected a borrowed Atom for valid UTF-8 input"),
213        }
214    }
215
216    #[test]
217    fn to_atom_lossy_returns_owned_for_invalid_utf8() {
218        let invalid_bytes = vec![0xed, 0xa0, 0x80];
219        let invalid = unsafe { Wtf8Buf::from_bytes_unchecked(invalid_bytes.clone()) };
220        let wtf = Wtf8Atom::new(invalid);
221
222        let lossy = wtf.to_string_lossy();
223
224        match wtf.to_atom_lossy() {
225            Cow::Borrowed(_) => panic!("expected an owned Atom for invalid UTF-8 input"),
226            Cow::Owned(atom) => {
227                assert_eq!(atom.as_ref(), lossy);
228            }
229        }
230    }
231}