1#[cfg(feature = "arbitrary")]
2use arbitrary::Arbitrary;
3use bounded_static::ToStatic;
4use nom::{
5 branch::alt,
6 bytes::complete::{tag, take, take_while1},
7 combinator::{map, opt, verify},
8 multi::many0,
9 sequence::{delimited, pair, preceded},
10 IResult,
11};
12use std::borrow::Cow;
13use std::fmt;
14#[cfg(feature = "arbitrary")]
15use std::ops::ControlFlow;
16#[cfg(feature = "tracing")]
17use tracing::warn;
18
19use crate::i18n::ContainsUtf8;
20use crate::print::{Formatter, Print, ToStringFromPrint};
21use crate::text::ascii;
22use crate::text::utf8::{is_nonascii_or, take_utf8_while1};
23use crate::text::whitespace::{cfws, fws, is_obs_no_ws_ctl};
24use crate::text::words::is_vchar;
25#[cfg(feature = "tracing-recover")]
26use crate::utils::bytes_to_trace_string;
27#[cfg(feature = "arbitrary")]
28use crate::{arbitrary_utils::arbitrary_string_where, fuzz_eq::FuzzEq};
29use eml_codec_derives::instrument_input;
30
31#[derive(Clone, ContainsUtf8, PartialEq, Default, ToStatic, ToStringFromPrint)]
33pub struct QuotedString<'a>(pub Vec<Cow<'a, str>>);
34
35impl<'a> fmt::Debug for QuotedString<'a> {
36 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
37 fmt.debug_tuple("QuotedString")
38 .field(&self.0.iter().collect::<Vec<_>>())
39 .finish()
40 }
41}
42
43impl<'a> QuotedString<'a> {
44 pub fn push_str(&mut self, e: &'a str) {
45 self.0.push(Cow::Borrowed(e))
46 }
47
48 pub fn push(&mut self, e: Cow<'a, str>) {
49 self.0.push(e)
50 }
51
52 pub fn chars<'b>(&'b self) -> QuotedStringChars<'a, 'b> {
53 QuotedStringChars {
54 q: self,
55 inner: QuotedStringCharsInner::NextFragment(0),
56 }
57 }
58}
59impl<'a> Print for QuotedString<'a> {
60 fn print(&self, fmt: &mut impl Formatter) {
61 print_quoted(fmt, self.chars())
62 }
63}
64impl<'a> TryFrom<&'a str> for QuotedString<'a> {
65 type Error = (); fn try_from(s: &'a str) -> Result<Self, Self::Error> {
67 if s.chars().all(is_strict_quoted_pair) {
68 Ok(Self(vec![s.into()]))
69 } else {
70 Err(())
71 }
72 }
73}
74
75#[cfg(feature = "arbitrary")]
76impl<'a> Arbitrary<'a> for QuotedString<'a> {
77 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
78 let mut chunks = Vec::new();
79 u.arbitrary_loop(None, Some(10), |u| {
80 let bytes = arbitrary_string_where(u, |c| is_vchar(c) || ascii::WS_CHAR.contains(&c))?;
81 chunks.push(Cow::Owned(bytes));
82 Ok(ControlFlow::Continue(()))
83 })?;
84 Ok(QuotedString(chunks))
85 }
86}
87
88#[cfg(feature = "arbitrary")]
89impl<'a> FuzzEq for QuotedString<'a> {
90 fn fuzz_eq(&self, other: &Self) -> bool {
91 self.chars().collect::<String>() == other.chars().collect::<String>()
92 }
93}
94
95#[derive(Clone)]
96pub struct QuotedStringChars<'a, 'b> {
97 q: &'b QuotedString<'a>,
98 inner: QuotedStringCharsInner<'b>,
99}
100#[derive(Clone)]
101enum QuotedStringCharsInner<'a> {
102 NextFragment(usize),
103 FragmentChars(usize, std::str::Chars<'a>),
104}
105
106impl<'a, 'b> Iterator for QuotedStringChars<'a, 'b> {
107 type Item = char;
108 fn next(&mut self) -> Option<Self::Item> {
109 match &mut self.inner {
110 QuotedStringCharsInner::NextFragment(idx) => match self.q.0.get(*idx) {
111 Some(frag) => {
112 self.inner = QuotedStringCharsInner::FragmentChars(*idx, frag.chars());
113 self.next()
114 }
115 None => None,
116 },
117 QuotedStringCharsInner::FragmentChars(idx, it) => match it.next() {
118 Some(c) => Some(c),
119 None => {
120 self.inner = QuotedStringCharsInner::NextFragment(*idx + 1);
121 self.next()
122 }
123 },
124 }
125 }
126}
127
128pub fn quoted_pair(input: &[u8]) -> IResult<&[u8], Option<&str>> {
141 preceded(
142 tag(&[ascii::BACKSLASH]),
143 map(
144 verify(take(1usize), |b: &[u8]| b[0].is_ascii()),
145 |s: &[u8]| {
146 let b = s[0];
147 if is_strict_quoted_pair(b.into()) {
148 Some(unsafe { str::from_utf8_unchecked(s) })
151 } else {
152 if !(b == ascii::NULL
153 || is_obs_no_ws_ctl(b)
154 || b == ascii::LF
155 || b == ascii::CR)
156 {
157 #[cfg(feature = "tracing-recover")]
158 warn!(byte = %bytes_to_trace_string(&[b]),
159 "invalid quoted pair")
160 }
161 None
162 }
163 },
164 ),
165 )(input)
166}
167
168fn is_strict_quoted_pair(c: char) -> bool {
169 is_vchar(c) || ascii::WS_CHAR.contains(&c)
170}
171
172fn is_strict_qtext(c: char) -> bool {
182 is_nonascii_or(|c| {
183 c == ascii::EXCLAMATION
184 || (ascii::NUM..=ascii::LEFT_BRACKET).contains(&c)
185 || (ascii::RIGHT_BRACKET..=ascii::TILDE).contains(&c)
186 })(c)
187}
188
189fn is_obs_qtext(c: u8) -> bool {
190 is_obs_no_ws_ctl(c)
191}
192
193#[instrument_input("tracing")]
202fn qcontent(input: &[u8]) -> IResult<&[u8], Option<Cow<'_, str>>> {
203 alt((
204 map(take_utf8_while1(is_strict_qtext), Some),
205 map(take_while1(is_obs_qtext), |_| None),
206 map(quoted_pair, |qp| qp.map(Cow::Borrowed)),
207 ))(input)
208}
209
210#[instrument_input("tracing")]
218pub fn quoted_string(input: &[u8]) -> IResult<&[u8], QuotedString<'_>> {
219 delimited(opt(cfws), quoted_string_plain, opt(cfws))(input)
220}
221pub fn quoted_string_plain(input: &[u8]) -> IResult<&[u8], QuotedString<'_>> {
222 let (input, _) = tag("\"")(input)?;
223 let (input, content) = many0(pair(opt(fws), qcontent))(input)?;
224 let (input, maybe_wsp) = opt(fws)(input)?;
225 let (input, _) = tag("\"")(input)?;
226
227 let mut qstring =
229 content
230 .into_iter()
231 .fold(QuotedString::default(), |mut acc, (maybe_wsp, c)| {
232 for wsp in maybe_wsp.into_iter().flat_map(|v| v.into_iter()) {
233 acc.push_str(wsp);
234 }
235 if let Some(c) = c {
236 acc.push(c);
237 }
238 acc
239 });
240
241 for wsp in maybe_wsp.into_iter().flat_map(|v| v.into_iter()) {
242 qstring.push_str(wsp);
243 }
244
245 Ok((input, qstring))
246}
247
248pub fn print_quoted<I>(fmt: &mut impl Formatter, data: I)
249where
250 I: IntoIterator<Item = char>,
251{
252 let mut buf = [0u8; 4];
253 fmt.write_bytes(b"\"");
254 for c in data.into_iter() {
255 let b = c.encode_utf8(&mut buf).as_bytes();
256 if is_strict_qtext(c) {
257 fmt.write_bytes(b);
258 } else if ascii::WS_CHAR.contains(&c) {
259 fmt.write_fws_bytes(b);
263 } else if is_vchar(c) {
264 fmt.write_bytes(b"\\");
265 fmt.write_bytes(b);
266 } else {
267 }
274 }
275 fmt.write_bytes(b"\"")
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::print::tests::print_to_vec_with;
282
283 #[test]
284 fn test_quoted_string_parser() {
285 assert_eq!(
286 quoted_string(b" \"hello\\\"world\" ").unwrap().1,
287 QuotedString(vec!["hello".into(), "\"".into(), "world".into(),])
288 );
289
290 assert_eq!(
291 quoted_string(b"\"hello\r\n world\""),
292 Ok((
293 &b""[..],
294 QuotedString(vec!["hello".into(), " ".into(), "world".into(),])
295 )),
296 );
297
298 assert_eq!(
299 quoted_string(b"\"\t\""),
300 Ok((&b""[..], QuotedString(vec!["\t".into(),]))),
301 );
302 }
303
304 #[test]
305 fn test_quoted_string_printer() {
306 let out = print_to_vec_with(|f| {
307 print_quoted(
308 f,
309 QuotedString(vec!["hello".into(), "\"".into(), " world".into()]).chars(),
310 );
311 });
312 assert_eq!(out, b"\"hello\\\" world\"");
313 }
314
315 #[test]
316 fn test_quoted_string_object() {
317 assert_eq!(
318 QuotedString(vec!["hello".into(), " ".into(), "world".into(),]).to_string(),
319 "\"hello world\"".to_string(),
320 );
321 }
322}