webdav_headers/
if_.rs

1// SPDX-FileCopyrightText: d-k-bo <d-k-bo@mailbox.org>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::{fmt::Display, str::FromStr};
6
7use nonempty::NonEmpty;
8
9use crate::{
10    utils::{HeaderIteratorExt, NonEmptyExt, ParseString, StrExt},
11    CodedUrl, IF,
12};
13
14pub use self::error::InvalidIf;
15
16/// The `If` header as defined in [RFC 4918](http://webdav.org/specs/rfc4918.html#HEADER_If).
17#[derive(Clone, Debug, PartialEq)]
18pub enum If {
19    NoTagList(Box<NonEmpty<NonEmpty<Condition>>>),
20    TaggedList(Box<NonEmpty<(ResourceTag, NonEmpty<NonEmpty<Condition>>)>>),
21}
22
23impl headers::Header for If {
24    fn name() -> &'static http::HeaderName {
25        &IF
26    }
27
28    fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
29    where
30        Self: Sized,
31        I: Iterator<Item = &'i http::HeaderValue>,
32    {
33        Ok(values.extract_str()?.parse()?)
34    }
35
36    fn encode<E: Extend<http::HeaderValue>>(&self, values: &mut E) {
37        values.extend(std::iter::once(self.to_string().parse().unwrap()))
38    }
39}
40
41impl Display for If {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        fn fmt_condition_lists(
44            f: &mut std::fmt::Formatter<'_>,
45            lists: &NonEmpty<NonEmpty<Condition>>,
46        ) -> std::fmt::Result {
47            for (i, conditions) in lists.iter().enumerate() {
48                if i > 0 {
49                    f.write_str(" ")?;
50                }
51
52                f.write_str("(")?;
53                for (j, condition) in conditions.iter().enumerate() {
54                    if j > 0 {
55                        f.write_str(" ")?;
56                    }
57                    condition.fmt(f)?;
58                }
59                f.write_str(")")?;
60            }
61            Ok(())
62        }
63
64        match self {
65            If::NoTagList(lists) => fmt_condition_lists(f, lists)?,
66            If::TaggedList(resources) => {
67                for (resource_tag, lists) in &**resources {
68                    resource_tag.fmt(f)?;
69                    f.write_str(" ")?;
70                    fmt_condition_lists(f, lists)?
71                }
72            }
73        }
74        Ok(())
75    }
76}
77
78impl FromStr for If {
79    type Err = InvalidIf;
80
81    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
82        Self::parse(&mut s)
83    }
84}
85
86impl ParseString for If {
87    type Err = InvalidIf;
88
89    fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
90        s = s.trim();
91
92        match ResourceTag::peek(s) {
93            Ok(_) => {
94                let resources = NonEmpty::try_collect(std::iter::from_fn(|| {
95                    (!s.is_empty()).then(|| {
96                        let resource_tag = ResourceTag::parse(&mut s)?;
97                        s = s.trim_start();
98
99                        let condition_lists = NonEmpty::try_collect(std::iter::from_fn(|| {
100                            (!s.is_empty() && !s.starts_with('<')).then(|| {
101                                let conditions = <NonEmpty<Condition>>::parse(&mut s)?;
102                                s = s.trim_start();
103                                Ok::<_, InvalidIf>(conditions)
104                            })
105                        }))?
106                        .ok_or(InvalidIf::EmptyConditionList)?;
107
108                        Ok::<_, InvalidIf>((resource_tag, condition_lists))
109                    })
110                }))?
111                .ok_or(InvalidIf::EmptyResourceList)?;
112
113                Ok((If::TaggedList(Box::new(resources)), s))
114            }
115            Err(_) => {
116                let condition_lists = NonEmpty::try_collect(std::iter::from_fn(|| {
117                    (!s.is_empty()).then(|| {
118                        let conditions = <NonEmpty<Condition>>::parse(&mut s)?;
119                        s = s.trim_start();
120                        Ok::<_, InvalidIf>(conditions)
121                    })
122                }))?
123                .ok_or(InvalidIf::EmptyConditionList)?;
124
125                Ok((If::NoTagList(Box::new(condition_lists)), s))
126            }
127        }
128    }
129}
130
131/// Condition used in the `If` header.
132#[derive(Clone, Debug, PartialEq)]
133pub enum Condition {
134    StateToken { not: bool, coded_url: CodedUrl },
135    ETag { not: bool, etag: String },
136}
137
138impl Display for Condition {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Condition::StateToken { not, coded_url } => {
142                if *not {
143                    f.write_str("Not ")?;
144                }
145                coded_url.fmt(f)?;
146            }
147            Condition::ETag { not, etag } => {
148                if *not {
149                    f.write_str("Not ")?;
150                }
151                f.write_fmt(format_args!("[{etag}]"))?;
152            }
153        }
154        Ok(())
155    }
156}
157
158impl ParseString for Condition {
159    type Err = InvalidIf;
160
161    fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
162        let not = s.starts_with_ignore_ascii_case("NOT");
163        if not {
164            s = s[3..].trim_start();
165        }
166
167        if s.starts_with('[') {
168            s = &s[1..];
169            let Some(end) = s.find(']') else {
170                return Err(InvalidIf::ExpectedChar(']'));
171            };
172
173            Ok((
174                Condition::ETag {
175                    not,
176                    etag: s[..end].to_owned(),
177                },
178                &s[end + 1..],
179            ))
180        } else {
181            Ok((
182                Condition::StateToken {
183                    not,
184                    coded_url: CodedUrl::parse(&mut s).map_err(InvalidIf::CodedUrl)?,
185                },
186                s,
187            ))
188        }
189    }
190}
191
192impl ParseString for NonEmpty<Condition> {
193    type Err = InvalidIf;
194
195    fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
196        if s.starts_with('(') {
197            s = s[1..].trim_start();
198        } else {
199            return Err(InvalidIf::ExpectedChar('('));
200        }
201        let conditions = NonEmpty::try_collect(std::iter::from_fn(|| {
202            (!s.starts_with(')')).then(|| {
203                let condition = Condition::parse(&mut s)?;
204                s = s.trim_start();
205                Ok::<_, InvalidIf>(condition)
206            })
207        }))?
208        .ok_or(InvalidIf::EmptyConditionList)?;
209
210        Ok((conditions, s[1..].trim_start()))
211    }
212}
213
214/// Resource tag used in the `If` header.
215#[derive(Clone, Debug, PartialEq)]
216pub struct ResourceTag(pub http::Uri);
217
218impl Display for ResourceTag {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        write!(f, "<{}>", self.0)
221    }
222}
223
224impl FromStr for ResourceTag {
225    type Err = InvalidIf;
226
227    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
228        Self::parse(&mut s)
229    }
230}
231
232impl ParseString for ResourceTag {
233    type Err = InvalidIf;
234
235    fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
236        if s.starts_with('<') {
237            s = &s[1..];
238        } else {
239            return Err(InvalidIf::ExpectedChar('<'));
240        }
241        let Some(end) = s.find('>') else {
242            return Err(InvalidIf::ExpectedChar('>'));
243        };
244
245        let uri = http::Uri::from_str(&s[..end]).map_err(InvalidIf::Uri)?;
246
247        Ok((ResourceTag(uri), &s[end + 1..]))
248    }
249}
250
251mod error {
252    use crate::InvalidCodedUrl;
253
254    /// Error returned when parsing [`If`](super::If) from a string fails.
255    #[derive(Debug)]
256    pub enum InvalidIf {
257        ExpectedChar(char),
258        EmptyConditionList,
259        EmptyResourceList,
260        CodedUrl(InvalidCodedUrl),
261        Uri(http::uri::InvalidUri),
262    }
263
264    impl std::fmt::Display for InvalidIf {
265        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266            match self {
267                Self::ExpectedChar(c) => write!(f, "expected '{c}'"),
268                Self::EmptyConditionList => f.write_str("empty condition list"),
269                Self::EmptyResourceList => f.write_str("empty resource list"),
270                Self::CodedUrl(..) => f.write_str("invalid Coded-URL"),
271                Self::Uri(..) => f.write_str("invalid URI"),
272            }
273        }
274    }
275
276    impl std::error::Error for InvalidIf {
277        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
278            match self {
279                Self::Uri(e) => Some(e),
280                _ => None,
281            }
282        }
283    }
284
285    impl From<InvalidIf> for headers::Error {
286        fn from(_: InvalidIf) -> Self {
287            headers::Error::invalid()
288        }
289    }
290}
291
292#[cfg(test)]
293#[test]
294fn test() {
295    use nonempty::nonempty;
296
297    use crate::test::test_all;
298
299    test_all([
300        (
301            // http://webdav.org/specs/rfc4918.html#if.header.evaluation.example.no-tag
302            r#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["I am an ETag"]) (["I am another ETag"])"#,
303            If::NoTagList(Box::new(nonempty![
304                nonempty![
305                    Condition::StateToken {
306                        not: false,
307                        coded_url: CodedUrl(
308                            uniresid::AbsoluteUri::parse(
309                                "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2"
310                            )
311                            .unwrap()
312                        )
313                    },
314                    Condition::ETag {
315                        not: false,
316                        etag: r#""I am an ETag""#.into()
317                    },
318                ],
319                nonempty![
320                    Condition::ETag {
321                        not: false,
322                        etag: r#""I am another ETag""#.into()
323                    },
324                ],
325            ])),
326        ),
327        (
328            // http://webdav.org/specs/rfc4918.html#rfc.section.10.4.7
329            "(Not <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> <urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)",
330            If::NoTagList(Box::new(nonempty![
331                nonempty![
332                    Condition::StateToken {
333                        not: true,
334                        coded_url: CodedUrl(
335                            uniresid::AbsoluteUri::parse("urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2")
336                                .unwrap()
337                        )
338                    },
339                    Condition::StateToken {
340                        not: false,
341                        coded_url: CodedUrl(
342                            uniresid::AbsoluteUri::parse("urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092")
343                                .unwrap()
344                        )
345                    },
346                ],
347            ]))
348        ),
349        (
350            // http://webdav.org/specs/rfc4918.html#rfc.section.10.4.9
351            r#"</resource1> (<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> [W/"A weak ETag"]) (["strong ETag"])"#,
352            If::TaggedList(Box::new(nonempty![
353                (
354                    ResourceTag("/resource1".parse().unwrap()) ,
355                    nonempty![
356                        nonempty![
357                            Condition::StateToken {
358                                not: false,
359                                coded_url: CodedUrl(
360                                    uniresid::AbsoluteUri::parse(
361                                        "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2"
362                                    )
363                                    .unwrap()
364                                )
365                            },
366                            Condition::ETag {
367                                not: false,
368                                etag: r#"W/"A weak ETag""#.into()
369                            }
370                        ],
371                        nonempty![Condition::ETag {
372                            not: false,
373                            etag: r#""strong ETag""#.into()
374                        }],
375                    ]
376                ),
377            ]))
378        ),
379    ]);
380}