aster_forge_webdav/
protocol.rs

1//! `WebDAV` header parsing and protocol precondition rules.
2
3use std::time::Duration;
4
5use http::header::HeaderMap;
6use http::uri::Authority;
7use http::{StatusCode, Uri};
8use percent_encoding::percent_decode_str;
9
10use crate::{
11    DavBackendError, DavFileSystem, DavIfResourceState, DavIfStateResolver, DavLockSystem, DavPath,
12    FsError,
13};
14use aster_forge_utils::http_validators;
15use async_trait::async_trait;
16
17/// `WebDAV` `Depth` header value.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Depth {
20    /// The request target only.
21    Zero,
22    /// The target and its immediate children.
23    One,
24    /// The complete descendant tree.
25    Infinity,
26}
27
28impl Depth {
29    /// Returns whether this depth traverses all descendants.
30    #[must_use]
31    pub fn is_infinity(self) -> bool {
32        matches!(self, Self::Infinity)
33    }
34}
35
36/// A parsed `Destination` header restricted to the current origin and mount.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Destination {
39    /// Canonical path relative to the `WebDAV` mount.
40    pub path: DavPath,
41    /// Decoded relative path retained for product adapters.
42    pub relative: String,
43}
44
45/// A parsed `WebDAV` `If` header.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct IfHeader {
48    /// Resource-tagged or untagged condition groups.
49    pub groups: Vec<IfResourceGroup>,
50}
51
52/// Conditions associated with one tagged resource or the request target.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct IfResourceGroup {
55    /// Tagged resource URI, or `None` for the request target.
56    pub tagged_path: Option<String>,
57    /// OR-connected state lists for this resource.
58    pub lists: Vec<IfStateList>,
59}
60
61/// AND-connected conditions inside one parenthesized state list.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct IfStateList {
64    /// State token and entity-tag conditions.
65    pub conditions: Vec<IfStateCondition>,
66}
67
68/// One `WebDAV` `If` condition.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum IfStateCondition {
71    /// A lock state token condition.
72    Token { value: String, negated: bool },
73    /// An entity-tag condition.
74    Etag { value: String, negated: bool },
75}
76
77/// Failure while resolving and evaluating a `WebDAV` `If` request precondition.
78#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79pub enum DavIfEvaluationError {
80    #[error(transparent)]
81    Protocol(#[from] DavProtocolError),
82    #[error(transparent)]
83    Backend(#[from] DavBackendError),
84}
85
86/// Stable protocol error classification for transport adapters.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum DavProtocolErrorKind {
89    BadRequest,
90    PreconditionFailed,
91}
92
93/// A product-neutral `WebDAV` protocol error.
94#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95#[error("{message}")]
96pub struct DavProtocolError {
97    kind: DavProtocolErrorKind,
98    status: StatusCode,
99    message: &'static str,
100}
101
102impl DavProtocolError {
103    /// Returns the stable error category.
104    #[must_use]
105    pub fn kind(&self) -> DavProtocolErrorKind {
106        self.kind
107    }
108
109    /// Returns the HTTP status required by the protocol boundary.
110    #[must_use]
111    pub fn status(&self) -> StatusCode {
112        self.status
113    }
114
115    /// Returns a protocol-level response message.
116    #[must_use]
117    pub fn message(&self) -> &'static str {
118        self.message
119    }
120
121    pub(crate) fn bad_request(message: &'static str) -> Self {
122        Self {
123            kind: DavProtocolErrorKind::BadRequest,
124            status: StatusCode::BAD_REQUEST,
125            message,
126        }
127    }
128
129    fn precondition_failed() -> Self {
130        Self {
131            kind: DavProtocolErrorKind::PreconditionFailed,
132            status: StatusCode::PRECONDITION_FAILED,
133            message: "Precondition failed",
134        }
135    }
136}
137
138/// Parses the `Depth` semantics used by `PROPFIND`.
139///
140/// # Errors
141///
142/// Returns an error when the `WebDAV` header is malformed or its condition fails.
143pub fn parse_propfind_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
144    match parse_depth_header(headers)? {
145        Some(Depth::Zero) => Ok(Depth::Zero),
146        Some(Depth::One) => Ok(Depth::One),
147        Some(Depth::Infinity) | None => Ok(Depth::Infinity),
148    }
149}
150
151/// Parses the `Depth` semantics used by `COPY`.
152///
153/// # Errors
154///
155/// Returns an error when the `WebDAV` header is malformed or its condition fails.
156pub fn parse_copy_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
157    match parse_depth_header(headers)? {
158        Some(Depth::Zero) => Ok(Depth::Zero),
159        Some(Depth::Infinity) | None => Ok(Depth::Infinity),
160        Some(Depth::One) => Err(DavProtocolError::bad_request("Invalid Depth header")),
161    }
162}
163
164/// Parses the `Depth` semantics used by `MOVE`.
165///
166/// # Errors
167///
168/// Returns an error when the `WebDAV` header is malformed or its condition fails.
169pub fn parse_move_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
170    Ok(parse_depth_header(headers)?.unwrap_or(Depth::Infinity))
171}
172
173/// Parses the `Depth` semantics used by `DELETE`.
174///
175/// # Errors
176///
177/// Returns an error when the `WebDAV` header is malformed or its condition fails.
178pub fn parse_delete_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
179    Ok(parse_depth_header(headers)?.unwrap_or(Depth::Infinity))
180}
181
182/// Parses the `Depth` semantics used by `LOCK`.
183///
184/// # Errors
185///
186/// Returns an error when the `WebDAV` header is malformed or its condition fails.
187pub fn parse_lock_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
188    match parse_depth_header(headers)? {
189        None | Some(Depth::Infinity) => Ok(Depth::Infinity),
190        Some(Depth::Zero) => Ok(Depth::Zero),
191        Some(Depth::One) => Err(DavProtocolError::bad_request("Invalid Depth header")),
192    }
193}
194
195fn parse_depth_header(headers: &HeaderMap) -> Result<Option<Depth>, DavProtocolError> {
196    let Some(value) = headers.get("Depth") else {
197        return Ok(None);
198    };
199    let value = value
200        .to_str()
201        .map_err(|_| DavProtocolError::bad_request("Invalid Depth header"))?;
202
203    match value {
204        value if value.eq_ignore_ascii_case("0") => Ok(Some(Depth::Zero)),
205        value if value.eq_ignore_ascii_case("1") => Ok(Some(Depth::One)),
206        value if value.eq_ignore_ascii_case("infinity") => Ok(Some(Depth::Infinity)),
207        _ => Err(DavProtocolError::bad_request("Invalid Depth header")),
208    }
209}
210
211/// Parses `Overwrite`, defaulting to `true` when the header is absent.
212///
213/// # Errors
214///
215/// Returns an error when the `WebDAV` header is malformed or its condition fails.
216pub fn parse_overwrite(headers: &HeaderMap) -> Result<bool, DavProtocolError> {
217    let Some(value) = headers.get("Overwrite") else {
218        return Ok(true);
219    };
220    let value = value
221        .to_str()
222        .map_err(|_| DavProtocolError::bad_request("Invalid Overwrite header"))?
223        .trim();
224    if value.eq_ignore_ascii_case("T") {
225        Ok(true)
226    } else if value.eq_ignore_ascii_case("F") {
227        Ok(false)
228    } else {
229        Err(DavProtocolError::bad_request("Invalid Overwrite header"))
230    }
231}
232
233/// Parses and constrains `Destination` to the current origin and `WebDAV` mount.
234///
235/// # Errors
236///
237/// Returns [`DavProtocolError`] when `Destination` is absent, malformed, or cross-origin.
238pub fn destination_relative_path(
239    headers: &HeaderMap,
240    prefix: &str,
241    request_scheme: &str,
242    request_host: &str,
243) -> Result<Destination, DavProtocolError> {
244    let raw = headers
245        .get("Destination")
246        .ok_or_else(|| DavProtocolError::bad_request("Missing Destination header"))?
247        .to_str()
248        .map_err(|_| DavProtocolError::bad_request("Invalid Destination header"))?
249        .trim();
250    let uri: Uri = raw
251        .parse()
252        .map_err(|_| DavProtocolError::bad_request("Invalid Destination header"))?;
253    match (uri.scheme_str(), uri.authority()) {
254        (Some(scheme), Some(authority)) => {
255            if !origin_authority_matches(scheme, authority, request_scheme, request_host) {
256                return Err(DavProtocolError::bad_request(
257                    "Destination must stay on this WebDAV server",
258                ));
259            }
260        }
261        (None, None) => {
262            if !raw.starts_with('/') {
263                return Err(DavProtocolError::bad_request("Invalid Destination header"));
264            }
265        }
266        _ => return Err(DavProtocolError::bad_request("Invalid Destination header")),
267    }
268
269    let path = uri.path();
270    let relative = strip_mount_prefix(path, prefix).ok_or_else(|| {
271        DavProtocolError::bad_request("Destination must stay under WebDAV prefix")
272    })?;
273    let path = DavPath::new(relative)
274        .map_err(|_| DavProtocolError::bad_request("Invalid Destination header"))?;
275    let relative = path.as_str().to_string();
276    Ok(Destination { path, relative })
277}
278
279/// Parses a `WebDAV` `If` header.
280///
281/// # Errors
282///
283/// Returns [`DavProtocolError`] when a DAV `If` field has invalid syntax or target paths.
284pub fn parse_if_header(headers: &HeaderMap) -> Result<Option<IfHeader>, DavProtocolError> {
285    let Some(value) = headers.get("If") else {
286        return Ok(None);
287    };
288    let raw = value
289        .to_str()
290        .map_err(|_| DavProtocolError::bad_request("Invalid If header"))?;
291    IfHeaderParser::new(raw).parse().map(Some)
292}
293
294/// Resolves referenced resources and enforces the complete `WebDAV` `If` state-list precondition.
295///
296/// Conditions inside one list are AND-connected. Lists for one resource and tagged resource
297/// productions across the complete header are OR-connected.
298///
299/// # Errors
300///
301/// Returns an error when resource state lookup fails or no submitted state list matches.
302pub async fn enforce_if_header(
303    if_header: Option<&IfHeader>,
304    resolver: &dyn DavIfStateResolver,
305    request_path: &DavPath,
306    prefix: &str,
307    request_scheme: &str,
308    request_host: &str,
309) -> Result<(), DavIfEvaluationError> {
310    let Some(if_header) = if_header else {
311        return Ok(());
312    };
313
314    for group in &if_header.groups {
315        let path = match group.tagged_path.as_deref() {
316            Some(tagged_path) => {
317                tagged_dav_path(prefix, tagged_path, request_scheme, request_host)?
318            }
319            None => Some(request_path.clone()),
320        };
321        let state = match path.as_ref() {
322            Some(path) => resolver.resolve_if_state(path).await?,
323            None => DavIfResourceState::default(),
324        };
325        if group
326            .lists
327            .iter()
328            .any(|list| evaluate_if_state_list(list, &state))
329        {
330            return Ok(());
331        }
332    }
333    Err(DavProtocolError::precondition_failed().into())
334}
335
336/// Resolves `If` state from the canonical filesystem and lock backend ports.
337///
338/// Products normally use this entrypoint instead of implementing a transport-local resolver.
339/// A missing resource contributes no `ETag`, while all other filesystem failures preserve the
340/// backend error boundary.
341///
342/// # Errors
343///
344/// Returns an error when filesystem or lock state lookup fails, or conditions do not match.
345pub async fn enforce_if_header_with_backends(
346    if_header: Option<&IfHeader>,
347    filesystem: &dyn DavFileSystem,
348    lock_system: &dyn DavLockSystem,
349    request_path: &DavPath,
350    prefix: &str,
351    request_scheme: &str,
352    request_host: &str,
353) -> Result<(), DavIfEvaluationError> {
354    let resolver = BackendIfStateResolver {
355        filesystem,
356        lock_system,
357    };
358    enforce_if_header(
359        if_header,
360        &resolver,
361        request_path,
362        prefix,
363        request_scheme,
364        request_host,
365    )
366    .await
367}
368
369struct BackendIfStateResolver<'a> {
370    filesystem: &'a dyn DavFileSystem,
371    lock_system: &'a dyn DavLockSystem,
372}
373
374#[async_trait]
375impl DavIfStateResolver for BackendIfStateResolver<'_> {
376    async fn resolve_if_state(
377        &self,
378        path: &DavPath,
379    ) -> Result<DavIfResourceState, DavBackendError> {
380        let etag = match self.filesystem.metadata(path).await {
381            Ok(metadata) => metadata.etag(),
382            Err(FsError::NotFound) => None,
383            Err(error) => return Err(error.into()),
384        };
385        let lock_tokens = self
386            .lock_system
387            .discover(path)
388            .await?
389            .into_iter()
390            .map(|lock| lock.token)
391            .collect();
392        Ok(DavIfResourceState { etag, lock_tokens })
393    }
394}
395
396fn evaluate_if_state_list(list: &IfStateList, state: &DavIfResourceState) -> bool {
397    list.conditions.iter().all(|condition| match condition {
398        IfStateCondition::Token { value, negated } => {
399            state.lock_tokens.iter().any(|token| token == value) ^ *negated
400        }
401        IfStateCondition::Etag { value, negated } => {
402            state.etag.as_deref().is_some_and(|etag| {
403                http_validators::if_none_match_header_matches(value, true, Some(etag))
404                    .unwrap_or(false)
405            }) ^ *negated
406        }
407    })
408}
409
410fn tagged_dav_path(
411    prefix: &str,
412    tagged_path: &str,
413    request_scheme: &str,
414    request_host: &str,
415) -> Result<Option<DavPath>, DavProtocolError> {
416    let uri: Uri = tagged_path
417        .parse()
418        .map_err(|_| DavProtocolError::bad_request("Invalid If header"))?;
419    let path = match (uri.scheme_str(), uri.authority()) {
420        (Some(scheme), Some(authority)) => {
421            if !origin_authority_matches(scheme, authority, request_scheme, request_host) {
422                return Ok(None);
423            }
424            uri.path()
425        }
426        (None, None) => uri.path(),
427        _ => return Err(DavProtocolError::bad_request("Invalid If header")),
428    };
429    if !path.starts_with('/') {
430        return Err(DavProtocolError::bad_request("Invalid If header"));
431    }
432    let Some(relative) = strip_mount_prefix(path, prefix) else {
433        return Ok(None);
434    };
435    DavPath::new(relative)
436        .map(Some)
437        .map_err(|_| DavProtocolError::bad_request("Invalid If header"))
438}
439
440/// Extracts submitted lock tokens that apply to one request path.
441#[must_use]
442pub fn submitted_lock_tokens_for_path(
443    headers: &HeaderMap,
444    request_path: &str,
445    request_scheme: &str,
446    request_host: &str,
447) -> Vec<String> {
448    let Some(if_header) = parse_if_header(headers).ok().flatten() else {
449        return Vec::new();
450    };
451    submitted_lock_tokens(&if_header, request_path, request_scheme, request_host)
452}
453
454/// Extracts submitted lock tokens from an already parsed `If` header.
455#[must_use]
456pub fn submitted_lock_tokens(
457    if_header: &IfHeader,
458    request_path: &str,
459    request_scheme: &str,
460    request_host: &str,
461) -> Vec<String> {
462    submitted_lock_tokens_matching_path(if_header, request_path, request_scheme, request_host, true)
463}
464
465pub(crate) fn submitted_mutation_lock_tokens(
466    if_header: &IfHeader,
467    request_path: &str,
468    request_scheme: &str,
469    request_host: &str,
470) -> Vec<String> {
471    submitted_lock_tokens_matching_path(
472        if_header,
473        request_path,
474        request_scheme,
475        request_host,
476        false,
477    )
478}
479
480fn submitted_lock_tokens_matching_path(
481    if_header: &IfHeader,
482    request_path: &str,
483    request_scheme: &str,
484    request_host: &str,
485    include_negated: bool,
486) -> Vec<String> {
487    let mut tokens = Vec::new();
488    for group in &if_header.groups {
489        match group.tagged_path.as_deref() {
490            None => {}
491            Some(tagged_path)
492                if if_tag_matches_path(tagged_path, request_path, request_scheme, request_host) => {
493            }
494            Some(_) => continue,
495        }
496        for list in &group.lists {
497            for condition in &list.conditions {
498                if let IfStateCondition::Token { value, negated } = condition
499                    && (include_negated || !negated)
500                {
501                    tokens.push(value.clone());
502                }
503            }
504        }
505    }
506    tokens.sort();
507    tokens.dedup();
508    tokens
509}
510
511/// Parses a bounded LOCK timeout using a product-supplied maximum duration.
512///
513/// # Errors
514///
515/// Returns an error when the `WebDAV` header is malformed or its condition fails.
516pub fn parse_lock_timeout(
517    headers: &HeaderMap,
518    maximum: Duration,
519) -> Result<Duration, DavProtocolError> {
520    let Some(value) = headers.get("Timeout") else {
521        return Ok(maximum);
522    };
523    let raw = value
524        .to_str()
525        .map_err(|_| DavProtocolError::bad_request("Invalid Timeout header"))?;
526    for candidate in raw
527        .split(',')
528        .map(str::trim)
529        .filter(|value| !value.is_empty())
530    {
531        if candidate.eq_ignore_ascii_case("Infinite") {
532            return Ok(maximum);
533        }
534        if let Some(seconds) = candidate
535            .strip_prefix("Second-")
536            .and_then(|seconds| seconds.parse::<u64>().ok())
537        {
538            return Ok(Duration::from_secs(seconds).min(maximum));
539        }
540    }
541    Err(DavProtocolError::bad_request("Invalid Timeout header"))
542}
543
544/// Parses the required angle-bracketed `Lock-Token` request header.
545///
546/// # Errors
547///
548/// Returns an error when the `WebDAV` header is malformed or its condition fails.
549pub fn parse_lock_token_header(headers: &HeaderMap) -> Result<String, DavProtocolError> {
550    let raw = headers
551        .get("Lock-Token")
552        .ok_or_else(|| DavProtocolError::bad_request("Missing Lock-Token header"))?
553        .to_str()
554        .map_err(|_| DavProtocolError::bad_request("Invalid Lock-Token header"))?
555        .trim();
556    let token = raw
557        .strip_prefix('<')
558        .and_then(|value| value.strip_suffix('>'))
559        .filter(|token| !token.is_empty() && !token.contains(['<', '>']))
560        .ok_or_else(|| DavProtocolError::bad_request("Invalid Lock-Token header"))?;
561    Ok(token.to_owned())
562}
563
564fn origin_authority_matches(
565    uri_scheme: &str,
566    uri_authority: &Authority,
567    request_scheme: &str,
568    request_host: &str,
569) -> bool {
570    if !uri_scheme.eq_ignore_ascii_case(request_scheme) {
571        return false;
572    }
573    let Ok(request_authority) = request_host.parse::<Authority>() else {
574        return false;
575    };
576    if uri_authority.as_str().contains('@') || request_authority.as_str().contains('@') {
577        return false;
578    }
579    uri_authority
580        .host()
581        .eq_ignore_ascii_case(request_authority.host())
582        && effective_port(uri_scheme, uri_authority)
583            == effective_port(request_scheme, &request_authority)
584}
585
586fn effective_port(scheme: &str, authority: &Authority) -> Option<u16> {
587    authority.port_u16().or_else(|| {
588        if scheme.eq_ignore_ascii_case("http") {
589            Some(80)
590        } else if scheme.eq_ignore_ascii_case("https") {
591            Some(443)
592        } else {
593            None
594        }
595    })
596}
597
598pub(crate) fn strip_mount_prefix<'a>(path: &'a str, prefix: &str) -> Option<&'a str> {
599    path.strip_prefix(prefix).filter(|_| {
600        prefix == "/"
601            || path == prefix
602            || path
603                .as_bytes()
604                .get(prefix.len())
605                .is_some_and(|byte| *byte == b'/')
606    })
607}
608
609fn normalize_lock_token(value: &str) -> String {
610    value
611        .trim()
612        .trim_matches(|character| character == '<' || character == '>')
613        .to_string()
614}
615
616struct IfHeaderParser<'a> {
617    input: &'a str,
618    position: usize,
619}
620
621impl<'a> IfHeaderParser<'a> {
622    fn new(input: &'a str) -> Self {
623        Self { input, position: 0 }
624    }
625
626    fn parse(&mut self) -> Result<IfHeader, DavProtocolError> {
627        self.skip_linear_whitespace();
628        if self.is_eof() {
629            return Err(DavProtocolError::bad_request("Invalid If header"));
630        }
631
632        let tagged = self.peek_char() == Some('<');
633        let mut groups = Vec::new();
634        if tagged {
635            while !self.is_eof() {
636                let tagged_path = self.parse_angle_value()?;
637                let mut lists = Vec::new();
638                loop {
639                    self.skip_linear_whitespace();
640                    if self.peek_char() != Some('(') {
641                        break;
642                    }
643                    lists.push(self.parse_state_list()?);
644                }
645                if lists.is_empty() {
646                    return Err(DavProtocolError::bad_request("Invalid If header"));
647                }
648                groups.push(IfResourceGroup {
649                    tagged_path: Some(tagged_path),
650                    lists,
651                });
652                self.skip_linear_whitespace();
653                if self.is_eof() {
654                    break;
655                }
656                if self.peek_char() != Some('<') {
657                    return Err(DavProtocolError::bad_request("Invalid If header"));
658                }
659            }
660        } else {
661            let mut lists = Vec::new();
662            while !self.is_eof() {
663                lists.push(self.parse_state_list()?);
664                self.skip_linear_whitespace();
665                if self.peek_char() == Some('<') {
666                    return Err(DavProtocolError::bad_request("Invalid If header"));
667                }
668            }
669            groups.push(IfResourceGroup {
670                tagged_path: None,
671                lists,
672            });
673        }
674        Ok(IfHeader { groups })
675    }
676
677    fn parse_state_list(&mut self) -> Result<IfStateList, DavProtocolError> {
678        self.expect_char('(')?;
679        let mut conditions = Vec::new();
680        loop {
681            self.skip_linear_whitespace();
682            if self.peek_char() == Some(')') {
683                self.position += 1;
684                break;
685            }
686            if self.is_eof() {
687                return Err(DavProtocolError::bad_request("Invalid If header"));
688            }
689
690            let negated = self.consume_not();
691            self.skip_linear_whitespace();
692            let condition = match self.peek_char() {
693                Some('<') => IfStateCondition::Token {
694                    value: normalize_lock_token(&self.parse_angle_value()?),
695                    negated,
696                },
697                Some('[') => IfStateCondition::Etag {
698                    value: self.parse_bracket_value()?,
699                    negated,
700                },
701                _ => return Err(DavProtocolError::bad_request("Invalid If header")),
702            };
703            conditions.push(condition);
704        }
705
706        if conditions.is_empty() {
707            return Err(DavProtocolError::bad_request("Invalid If header"));
708        }
709        Ok(IfStateList { conditions })
710    }
711
712    fn parse_angle_value(&mut self) -> Result<String, DavProtocolError> {
713        self.parse_delimited('<', '>')
714    }
715
716    fn parse_bracket_value(&mut self) -> Result<String, DavProtocolError> {
717        self.parse_delimited('[', ']')
718    }
719
720    fn parse_delimited(
721        &mut self,
722        opening: char,
723        closing: char,
724    ) -> Result<String, DavProtocolError> {
725        self.expect_char(opening)?;
726        let start = self.position;
727        while let Some(character) = self.peek_char() {
728            if character == closing {
729                let value = self.input[start..self.position].trim();
730                self.position += closing.len_utf8();
731                if value.is_empty() {
732                    return Err(DavProtocolError::bad_request("Invalid If header"));
733                }
734                return Ok(value.to_string());
735            }
736            self.position += character.len_utf8();
737        }
738        Err(DavProtocolError::bad_request("Invalid If header"))
739    }
740
741    fn consume_not(&mut self) -> bool {
742        let rest = &self.input[self.position..];
743        let Some(candidate) = rest.get(..3) else {
744            return false;
745        };
746        if !candidate.eq_ignore_ascii_case("not") {
747            return false;
748        }
749        let after_not = &rest[3..];
750        if after_not.chars().next().is_some_and(|character| {
751            !character.is_ascii_whitespace() && character != '<' && character != '['
752        }) {
753            return false;
754        }
755        self.position += 3;
756        true
757    }
758
759    fn expect_char(&mut self, expected: char) -> Result<(), DavProtocolError> {
760        if self.peek_char() == Some(expected) {
761            self.position += expected.len_utf8();
762            Ok(())
763        } else {
764            Err(DavProtocolError::bad_request("Invalid If header"))
765        }
766    }
767
768    fn skip_linear_whitespace(&mut self) {
769        while self
770            .peek_char()
771            .is_some_and(|character| matches!(character, ' ' | '\t' | '\r' | '\n'))
772        {
773            self.position += 1;
774        }
775    }
776
777    fn peek_char(&self) -> Option<char> {
778        self.input[self.position..].chars().next()
779    }
780
781    fn is_eof(&self) -> bool {
782        self.position >= self.input.len()
783    }
784}
785
786fn if_tag_matches_path(
787    tagged_path: &str,
788    request_path: &str,
789    request_scheme: &str,
790    request_host: &str,
791) -> bool {
792    if path_equivalent(tagged_path, request_path) {
793        return true;
794    }
795    let Ok(uri) = tagged_path.parse::<Uri>() else {
796        return false;
797    };
798    match (uri.scheme_str(), uri.authority()) {
799        (Some(scheme), Some(authority)) => {
800            origin_authority_matches(scheme, authority, request_scheme, request_host)
801                && path_equivalent(uri.path(), request_path)
802        }
803        (None, None) => path_equivalent(uri.path(), request_path),
804        _ => false,
805    }
806}
807
808fn path_equivalent(left: &str, right: &str) -> bool {
809    if left == right {
810        return true;
811    }
812    let left_decoded = percent_decode_str(left).decode_utf8().ok();
813    let right_decoded = percent_decode_str(right).decode_utf8().ok();
814    match (left_decoded.as_deref(), right_decoded.as_deref()) {
815        (Some(left), Some(right)) => left == right,
816        (Some(left), None) => left == right,
817        (None, Some(right)) => left == right,
818        (None, None) => false,
819    }
820}