aster_forge_webdav/
response.rs

1//! Transport-neutral `WebDAV` response model and download response planning.
2
3use std::fmt::Write as _;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::task::{Context, Poll};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use aster_forge_utils::http_range::{
10    HttpByteRange, HttpRangeError, parse_byte_ranges, parse_single_byte_range,
11};
12use aster_forge_utils::http_validators::{http_date_epoch_seconds, parse_http_date};
13use bytes::Bytes;
14use futures::Stream;
15use http::header::{
16    ACCEPT_RANGES, ALLOW, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE,
17    CONTENT_TYPE, IF_RANGE, RANGE,
18};
19use http::{HeaderMap, HeaderValue, StatusCode};
20
21use crate::DavMultiStatusStream;
22use crate::{
23    DavBackendError, DavBackendErrorKind, DavCapabilityEvaluationError, DavCapabilitySnapshot,
24    DavConditionalOutcome, DavConditionalPlan, DavConditionalPlanError, DavConditionalResource,
25    DavContentStream, DavDownloadOpenError, DavDownloadSource, DavErrorCondition, DavMethod,
26    DavMethodGateError, DavOpenedDownload, DavPath, DavProtocolError, DavProtocolErrorKind,
27    DavRangeEvaluation, DavXmlElement, DavXmlError, dav_error_element, plan_http_conditionals,
28};
29
30/// Failure while enforcing a request body policy in the transport adapter.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
32pub enum DavBodyError {
33    #[error("failed to read WebDAV request body")]
34    ReadFailed,
35    #[error("WebDAV request body is too large")]
36    BodyTooLarge,
37    #[error("WebDAV method does not accept a request body")]
38    BodyNotAllowed,
39}
40
41/// Whether a successful GET/HEAD response needs content from the product backend.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum DavDownloadBody {
44    /// The response has no body because it is a HEAD, 304, or 416 response.
45    Empty,
46    /// Stream the complete representation with its planned output length.
47    Full { expected_length: u64 },
48    /// Stream only the selected representation range.
49    Range(HttpByteRange),
50    /// Stream the selected ranges with RFC 9110 `multipart/byteranges` framing.
51    Multipart(DavMultipartDownloadPlan),
52}
53
54/// Hard bounds applied before any multi-range backend stream is opened.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct DavMultiRangeLimits {
57    pub maximum_header_bytes: usize,
58    /// Maximum raw range specs accepted before coalescing allocates its result.
59    ///
60    /// Coalescing preserves request order and has superlinear worst-case CPU cost. Products must
61    /// choose this bound explicitly; raising it can significantly increase per-request work and
62    /// there is no implicit fallback limit.
63    pub maximum_raw_ranges: usize,
64    pub maximum_segments: usize,
65    pub maximum_aggregate_bytes: u64,
66    pub maximum_backend_opens: usize,
67}
68
69impl DavMultiRangeLimits {
70    #[must_use]
71    pub const fn new(
72        maximum_header_bytes: usize,
73        maximum_raw_ranges: usize,
74        maximum_segments: usize,
75        maximum_aggregate_bytes: u64,
76        maximum_backend_opens: usize,
77    ) -> Self {
78        Self {
79            maximum_header_bytes,
80            maximum_raw_ranges,
81            maximum_segments,
82            maximum_aggregate_bytes,
83            maximum_backend_opens,
84        }
85    }
86}
87
88/// Stable response policy when an otherwise valid multi-range request exceeds a hard bound.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum DavRangeLimitBehavior {
91    /// Ignore `Range` and serve the complete representation with `200 OK`.
92    IgnoreRange,
93    /// Reject the selected range-set with `416 Range Not Satisfiable`.
94    RangeNotSatisfiable,
95}
96
97/// Explicit policy that enables bounded multi-range response planning for one resource.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct DavMultiRangePolicy {
100    pub limits: DavMultiRangeLimits,
101    /// Merge overlap, adjacency, and gaps no larger than this number of representation bytes.
102    pub coalesce_gap_bytes: u64,
103    pub limit_behavior: DavRangeLimitBehavior,
104}
105
106impl DavMultiRangePolicy {
107    #[must_use]
108    pub const fn new(
109        limits: DavMultiRangeLimits,
110        coalesce_gap_bytes: u64,
111        limit_behavior: DavRangeLimitBehavior,
112    ) -> Self {
113        Self {
114            limits,
115            coalesce_gap_bytes,
116            limit_behavior,
117        }
118    }
119}
120
121/// One final backend range and its framing location in a multipart plan.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct DavMultipartSegmentPlan {
124    range: HttpByteRange,
125    frame_start: usize,
126    frame_end: usize,
127}
128
129impl DavMultipartSegmentPlan {
130    #[must_use]
131    pub const fn range(&self) -> HttpByteRange {
132        self.range
133    }
134}
135
136/// Bounded multipart framing and final backend range-open plan.
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct DavMultipartDownloadPlan {
139    requested_range_count: usize,
140    selected_length: u64,
141    expected_length: u64,
142    segments: Vec<DavMultipartSegmentPlan>,
143    framing: Bytes,
144    closing_start: usize,
145}
146
147impl DavMultipartDownloadPlan {
148    /// Returns the sender's non-empty raw range-spec count.
149    #[must_use]
150    pub const fn requested_range_count(&self) -> usize {
151        self.requested_range_count
152    }
153
154    /// Returns the final coalesced range-open plans in response order.
155    #[must_use]
156    pub fn segments(&self) -> &[DavMultipartSegmentPlan] {
157        &self.segments
158    }
159
160    /// Returns the total representation bytes selected across all final segments.
161    #[must_use]
162    pub const fn selected_length(&self) -> u64 {
163        self.selected_length
164    }
165
166    /// Returns the exact multipart body length, including framing.
167    #[must_use]
168    pub const fn expected_length(&self) -> u64 {
169        self.expected_length
170    }
171}
172
173/// A complete response shell plus the storage read selected by the protocol layer.
174pub struct DavDownloadPlan {
175    pub response: DavResponse,
176    pub body: DavDownloadBody,
177}
178
179/// Failure while building a product-neutral download response plan.
180#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
181pub enum DavDownloadPlanError {
182    #[error(transparent)]
183    Protocol(#[from] DavProtocolError),
184    #[error("invalid WebDAV download representation metadata")]
185    InvalidRepresentation,
186}
187
188/// `WebDAV` response body before transport adaptation.
189pub enum DavResponseBody {
190    Empty,
191    Bytes(Bytes),
192    Stream(DavContentStream),
193    MultiStatus(DavMultiStatusStream),
194}
195
196/// Status, headers, and body produced by the protocol layer.
197pub struct DavResponse {
198    pub status: StatusCode,
199    pub headers: HeaderMap,
200    pub body: DavResponseBody,
201}
202
203impl DavResponse {
204    /// Creates an empty response.
205    #[must_use]
206    pub fn empty(status: StatusCode) -> Self {
207        Self {
208            status,
209            headers: HeaderMap::new(),
210            body: DavResponseBody::Empty,
211        }
212    }
213
214    /// Creates a byte response.
215    #[must_use]
216    pub fn bytes(status: StatusCode, body: impl Into<Bytes>) -> Self {
217        Self {
218            status,
219            headers: HeaderMap::new(),
220            body: DavResponseBody::Bytes(body.into()),
221        }
222    }
223}
224
225pub(crate) fn xml_document_response(
226    status: StatusCode,
227    root: &DavXmlElement,
228) -> Result<DavResponse, DavXmlError> {
229    let mut response = DavResponse::bytes(status, root.to_bytes()?);
230    response.headers.insert(
231        CONTENT_TYPE,
232        HeaderValue::from_static("application/xml; charset=utf-8"),
233    );
234    if status.is_client_error() || status.is_server_error() {
235        response
236            .headers
237            .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
238    }
239    Ok(response)
240}
241
242pub(crate) fn text_document_response(status: StatusCode, body: impl Into<String>) -> DavResponse {
243    let mut response = DavResponse::bytes(status, body.into());
244    response.headers.insert(
245        CONTENT_TYPE,
246        HeaderValue::from_static("text/plain; charset=utf-8"),
247    );
248    if status.is_client_error() || status.is_server_error() {
249        response
250            .headers
251            .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
252    }
253    response
254}
255
256pub(crate) fn xml_request_error_response(
257    error: DavXmlError,
258    invalid_grammar_message: &'static str,
259) -> Result<DavResponse, DavXmlError> {
260    match error {
261        DavXmlError::ExternalEntity => xml_document_response(
262            StatusCode::FORBIDDEN,
263            &dav_error_element(&DavErrorCondition::NoExternalEntities),
264        ),
265        DavXmlError::TooLarge => Ok(text_document_response(
266            StatusCode::PAYLOAD_TOO_LARGE,
267            "WebDAV XML body too large",
268        )),
269        DavXmlError::TooDeep | DavXmlError::Malformed => Ok(text_document_response(
270            StatusCode::BAD_REQUEST,
271            "Invalid XML body",
272        )),
273        DavXmlError::InvalidGrammar => Ok(text_document_response(
274            StatusCode::BAD_REQUEST,
275            invalid_grammar_message,
276        )),
277    }
278}
279
280/// Maps a protocol parsing/precondition failure to its transport-neutral response.
281#[must_use]
282pub fn protocol_error_response(error: &DavProtocolError) -> DavResponse {
283    match error.kind() {
284        DavProtocolErrorKind::BadRequest => text_document_response(error.status(), error.message()),
285        DavProtocolErrorKind::PreconditionFailed => no_store_empty_response(error.status()),
286    }
287}
288
289/// Maps conditional request parsing or representation failures to a response.
290#[must_use]
291pub fn conditional_plan_error_response(error: &DavConditionalPlanError) -> DavResponse {
292    match error {
293        DavConditionalPlanError::Protocol(error) => protocol_error_response(error),
294        DavConditionalPlanError::InvalidRepresentation => {
295            no_store_empty_response(StatusCode::INTERNAL_SERVER_ERROR)
296        }
297    }
298}
299
300/// Maps provider lookup or invalid product capability declarations to a response.
301#[must_use]
302pub fn capability_evaluation_error_response(error: &DavCapabilityEvaluationError) -> DavResponse {
303    match error {
304        DavCapabilityEvaluationError::Backend(error) => backend_error_response(error),
305        DavCapabilityEvaluationError::Plan(_) => {
306            no_store_empty_response(StatusCode::INTERNAL_SERVER_ERROR)
307        }
308    }
309}
310
311/// Maps a classified product backend failure to the `WebDAV` status contract.
312#[must_use]
313pub fn backend_error_response(error: &DavBackendError) -> DavResponse {
314    let status = match error.kind {
315        DavBackendErrorKind::NotFound => StatusCode::NOT_FOUND,
316        DavBackendErrorKind::Forbidden => StatusCode::FORBIDDEN,
317        DavBackendErrorKind::Conflict | DavBackendErrorKind::AlreadyExists => StatusCode::CONFLICT,
318        DavBackendErrorKind::InsufficientStorage => StatusCode::INSUFFICIENT_STORAGE,
319        DavBackendErrorKind::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
320        DavBackendErrorKind::Locked => StatusCode::LOCKED,
321        DavBackendErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
322        DavBackendErrorKind::Unsupported => StatusCode::METHOD_NOT_ALLOWED,
323        DavBackendErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
324    };
325    no_store_empty_response(status)
326}
327
328pub(crate) fn no_store_empty_response(status: StatusCode) -> DavResponse {
329    let mut response = DavResponse::empty(status);
330    response
331        .headers
332        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
333    response
334}
335
336/// Builds the response to a DAV `OPTIONS` request.
337#[must_use]
338pub fn options_response(snapshot: &DavCapabilitySnapshot) -> DavResponse {
339    let mut response = DavResponse::empty(StatusCode::OK);
340    response
341        .headers
342        .insert(ALLOW, snapshot.allow_header().clone());
343    if let Some(dav) = snapshot.dav_header() {
344        response.headers.insert("DAV", dav.clone());
345    }
346    if let Some(dasl) = snapshot.dasl_header() {
347        response.headers.insert("DASL", dasl.clone());
348    }
349    if let Some(accept_patch) = snapshot.accept_patch_header() {
350        response
351            .headers
352            .insert("Accept-Patch", accept_patch.clone());
353    }
354    if snapshot.has_ms_author_via() {
355        response
356            .headers
357            .insert("MS-Author-Via", HeaderValue::from_static("DAV"));
358    }
359    response
360}
361
362/// Builds the response for an unsupported HTTP/WebDAV method.
363#[must_use]
364pub fn method_not_allowed_response(snapshot: &DavCapabilitySnapshot) -> DavResponse {
365    let mut response = DavResponse::empty(StatusCode::METHOD_NOT_ALLOWED);
366    response
367        .headers
368        .insert(ALLOW, snapshot.allow_header().clone());
369    response
370        .headers
371        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
372    response
373}
374
375/// Gates known and unknown methods through the snapshot's protocol dispatch projection.
376///
377/// # Errors
378///
379/// Returns [`DavMethodGateError`] when the method is unknown or disallowed by the snapshot.
380pub fn gate_method(
381    method: Option<DavMethod>,
382    snapshot: &DavCapabilitySnapshot,
383) -> Result<DavMethod, DavMethodGateError> {
384    match method {
385        Some(method) if snapshot.dispatches(method) => Ok(method),
386        Some(_) | None => Err(DavMethodGateError::MethodNotAllowed),
387    }
388}
389
390/// Builds the protocol response for a transport body-policy failure.
391#[must_use]
392pub fn body_error_response(error: DavBodyError) -> DavResponse {
393    let (status, body) = match error {
394        DavBodyError::ReadFailed => (StatusCode::BAD_REQUEST, Some("Failed to read request body")),
395        DavBodyError::BodyTooLarge => (
396            StatusCode::PAYLOAD_TOO_LARGE,
397            Some("WebDAV request body too large"),
398        ),
399        DavBodyError::BodyNotAllowed => (StatusCode::UNSUPPORTED_MEDIA_TYPE, None),
400    };
401    let mut response = match body {
402        Some(body) => DavResponse::bytes(status, body),
403        None => DavResponse::empty(status),
404    };
405    response
406        .headers
407        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
408    if body.is_some() {
409        response.headers.insert(
410            CONTENT_TYPE,
411            HeaderValue::from_static("text/plain; charset=utf-8"),
412        );
413    }
414    response
415}
416
417/// Builds the GET/HEAD response and storage-read plan after product metadata has been resolved.
418///
419/// # Errors
420///
421/// Returns [`DavDownloadPlanError`] when validators, range input, or response headers are invalid.
422pub fn plan_download_response(
423    headers: &HeaderMap,
424    head_only: bool,
425    content_length: u64,
426    content_type: &str,
427    etag: Option<&str>,
428    last_modified: SystemTime,
429) -> Result<DavDownloadPlan, DavDownloadPlanError> {
430    plan_download_response_with_mode(
431        headers,
432        head_only,
433        content_length,
434        content_type,
435        etag,
436        last_modified,
437        DavRangePlanningMode::SingleOnly,
438    )
439}
440
441/// Builds a GET/HEAD plan with an explicitly bounded multi-range policy.
442///
443/// Every limit and the final coalescing pass are applied before [`open_download`] opens a
444/// backend stream. A request containing one raw range retains the ordinary single-part path.
445///
446/// # Errors
447///
448/// Returns an error when validators, ranges, limits, or response headers are invalid.
449pub fn plan_download_response_with_multi_range(
450    headers: &HeaderMap,
451    head_only: bool,
452    content_length: u64,
453    content_type: &str,
454    etag: Option<&str>,
455    last_modified: SystemTime,
456    policy: DavMultiRangePolicy,
457) -> Result<DavDownloadPlan, DavDownloadPlanError> {
458    plan_download_response_with_mode(
459        headers,
460        head_only,
461        content_length,
462        content_type,
463        etag,
464        last_modified,
465        DavRangePlanningMode::Multi(policy),
466    )
467}
468
469#[derive(Clone, Copy)]
470enum DavRangePlanningMode {
471    SingleOnly,
472    Multi(DavMultiRangePolicy),
473}
474
475enum DavRangeSelection {
476    Full,
477    NotSatisfiable,
478    Single(HttpByteRange),
479    Multipart {
480        requested_range_count: usize,
481        selected_length: u64,
482        ranges: Vec<HttpByteRange>,
483    },
484}
485
486#[expect(
487    clippy::too_many_lines,
488    reason = "Conditional precedence, range selection, and response headers form one RFC planning transaction."
489)]
490fn plan_download_response_with_mode(
491    headers: &HeaderMap,
492    head_only: bool,
493    content_length: u64,
494    content_type: &str,
495    etag: Option<&str>,
496    last_modified: SystemTime,
497    range_mode: DavRangePlanningMode,
498) -> Result<DavDownloadPlan, DavDownloadPlanError> {
499    let conditional = plan_http_conditionals(
500        if head_only {
501            DavMethod::Head
502        } else {
503            DavMethod::Get
504        },
505        headers,
506        DavConditionalResource {
507            exists: true,
508            etag,
509            last_modified: Some(last_modified),
510        },
511    )
512    .map_err(|error| match error {
513        DavConditionalPlanError::Protocol(error) => DavDownloadPlanError::Protocol(error),
514        DavConditionalPlanError::InvalidRepresentation => {
515            DavDownloadPlanError::InvalidRepresentation
516        }
517    })?;
518    match conditional.outcome {
519        DavConditionalOutcome::Proceed => {}
520        DavConditionalOutcome::NotModified => {
521            let mut response = DavResponse::empty(StatusCode::NOT_MODIFIED);
522            conditional.apply_response_headers(response.status, &mut response.headers);
523            return Ok(DavDownloadPlan {
524                response,
525                body: DavDownloadBody::Empty,
526            });
527        }
528        DavConditionalOutcome::PreconditionFailed => {
529            let mut response = no_store_empty_response(StatusCode::PRECONDITION_FAILED);
530            conditional.apply_response_headers(response.status, &mut response.headers);
531            return Ok(DavDownloadPlan {
532                response,
533                body: DavDownloadBody::Empty,
534            });
535        }
536    }
537
538    let range = if conditional.range == DavRangeEvaluation::Skip
539        || !if_range_matches(headers, etag, last_modified)
540    {
541        DavRangeSelection::Full
542    } else {
543        plan_requested_range(headers, content_length, range_mode)?
544    };
545    let (status, response_length, response_content_type, content_range, body) = match range {
546        DavRangeSelection::Single(range) => (
547            StatusCode::PARTIAL_CONTENT,
548            range.length(),
549            header_value(content_type)?,
550            Some(range.content_range_header()),
551            DavDownloadBody::Range(range),
552        ),
553        DavRangeSelection::Multipart {
554            requested_range_count,
555            selected_length,
556            ranges,
557        } => {
558            let (plan, multipart_content_type) =
559                build_multipart_plan(requested_range_count, selected_length, ranges, content_type)?;
560            (
561                StatusCode::PARTIAL_CONTENT,
562                plan.expected_length,
563                multipart_content_type,
564                None,
565                DavDownloadBody::Multipart(plan),
566            )
567        }
568        DavRangeSelection::Full if head_only => (
569            StatusCode::OK,
570            content_length,
571            header_value(content_type)?,
572            None,
573            DavDownloadBody::Empty,
574        ),
575        DavRangeSelection::Full => (
576            StatusCode::OK,
577            content_length,
578            header_value(content_type)?,
579            None,
580            DavDownloadBody::Full {
581                expected_length: content_length,
582            },
583        ),
584        DavRangeSelection::NotSatisfiable => {
585            return Ok(range_not_satisfiable_plan(content_length, &conditional));
586        }
587    };
588    let mut response = DavResponse::empty(status);
589    response
590        .headers
591        .insert(CONTENT_LENGTH, header_value(&response_length.to_string())?);
592    response.headers.insert(CONTENT_TYPE, response_content_type);
593    response
594        .headers
595        .insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
596    response
597        .headers
598        .insert(CONTENT_ENCODING, HeaderValue::from_static("identity"));
599    if let Some(content_range) = content_range {
600        response
601            .headers
602            .insert(CONTENT_RANGE, header_value(&content_range)?);
603    }
604    conditional.apply_response_headers(response.status, &mut response.headers);
605
606    Ok(DavDownloadPlan { response, body })
607}
608
609/// Opens the storage stream selected by a download plan and verifies its declared length.
610///
611/// Full, single-range, and every final multipart segment carry their exact selected length.
612/// Multipart segments are all opened and checked before the returned stream can emit bytes;
613/// empty plans never call the backend.
614///
615/// # Errors
616///
617/// Returns [`DavDownloadOpenError`] when a backend open fails or its length is inconsistent.
618pub async fn open_download<Source: DavDownloadSource>(
619    source: &Source,
620    path: &DavPath,
621    body: DavDownloadBody,
622) -> Result<Option<DavOpenedDownload>, DavDownloadOpenError> {
623    let (opened, planned_length) = match body {
624        DavDownloadBody::Empty => return Ok(None),
625        DavDownloadBody::Full { expected_length } => {
626            (source.open_full(path).await?, expected_length)
627        }
628        DavDownloadBody::Range(range) => (source.open_range(path, range).await?, range.length()),
629        DavDownloadBody::Multipart(plan) => {
630            return open_multipart_download(source, path, plan).await;
631        }
632    };
633    if opened.expected_length != planned_length {
634        return Err(DavDownloadOpenError::LengthMismatch {
635            planned: planned_length,
636            opened: opened.expected_length,
637        });
638    }
639    Ok(Some(opened))
640}
641
642fn plan_requested_range(
643    headers: &HeaderMap,
644    content_length: u64,
645    mode: DavRangePlanningMode,
646) -> Result<DavRangeSelection, DavDownloadPlanError> {
647    match mode {
648        DavRangePlanningMode::SingleOnly => {
649            let Some(value) = headers.get(RANGE) else {
650                return Ok(DavRangeSelection::Full);
651            };
652            let Ok(raw) = value.to_str() else {
653                return Ok(DavRangeSelection::NotSatisfiable);
654            };
655            Ok(match parse_single_byte_range(raw, content_length) {
656                Ok(range) => DavRangeSelection::Single(range),
657                Err(
658                    HttpRangeError::UnsupportedUnit
659                    | HttpRangeError::MultipleRangesUnsupported
660                    | HttpRangeError::HeaderTooLong
661                    | HttpRangeError::TooManyRanges,
662                ) => DavRangeSelection::Full,
663                Err(
664                    HttpRangeError::Malformed
665                    | HttpRangeError::InvalidNumber
666                    | HttpRangeError::EmptyRepresentation
667                    | HttpRangeError::Unsatisfiable,
668                ) => DavRangeSelection::NotSatisfiable,
669            })
670        }
671        DavRangePlanningMode::Multi(policy) => plan_multi_range(headers, content_length, policy),
672    }
673}
674
675fn plan_multi_range(
676    headers: &HeaderMap,
677    content_length: u64,
678    policy: DavMultiRangePolicy,
679) -> Result<DavRangeSelection, DavDownloadPlanError> {
680    let mut values = headers.get_all(RANGE).iter();
681    let Some(value) = values.next() else {
682        return Ok(DavRangeSelection::Full);
683    };
684    if values.next().is_some() {
685        return Ok(DavRangeSelection::NotSatisfiable);
686    }
687    if value.as_bytes().len() > policy.limits.maximum_header_bytes {
688        return Ok(range_limit_selection(policy.limit_behavior));
689    }
690    let Ok(raw) = value.to_str() else {
691        return Ok(DavRangeSelection::NotSatisfiable);
692    };
693    let set = match parse_byte_ranges(
694        raw,
695        content_length,
696        policy.limits.maximum_header_bytes,
697        policy.limits.maximum_raw_ranges,
698    ) {
699        Ok(set) => set,
700        Err(HttpRangeError::UnsupportedUnit) => return Ok(DavRangeSelection::Full),
701        Err(HttpRangeError::HeaderTooLong | HttpRangeError::TooManyRanges) => {
702            return Ok(range_limit_selection(policy.limit_behavior));
703        }
704        Err(
705            HttpRangeError::MultipleRangesUnsupported
706            | HttpRangeError::Malformed
707            | HttpRangeError::InvalidNumber
708            | HttpRangeError::EmptyRepresentation
709            | HttpRangeError::Unsatisfiable,
710        ) => return Ok(DavRangeSelection::NotSatisfiable),
711    };
712    let requested_range_count = set.requested_count();
713    if requested_range_count == 1 {
714        return Ok(set
715            .ranges()
716            .first()
717            .copied()
718            .map_or(DavRangeSelection::NotSatisfiable, DavRangeSelection::Single));
719    }
720
721    let ranges = coalesce_ranges(set.into_ranges(), policy.coalesce_gap_bytes)?;
722    if ranges.len() > policy.limits.maximum_segments
723        || ranges.len() > policy.limits.maximum_backend_opens
724    {
725        return Ok(range_limit_selection(policy.limit_behavior));
726    }
727    let mut selected_length = 0u64;
728    for range in &ranges {
729        // Coalesced ranges are non-overlapping intervals in one u64-sized representation.
730        selected_length += range.length();
731    }
732    if selected_length > policy.limits.maximum_aggregate_bytes {
733        return Ok(range_limit_selection(policy.limit_behavior));
734    }
735
736    Ok(DavRangeSelection::Multipart {
737        requested_range_count,
738        selected_length,
739        ranges,
740    })
741}
742
743const fn range_limit_selection(behavior: DavRangeLimitBehavior) -> DavRangeSelection {
744    match behavior {
745        DavRangeLimitBehavior::IgnoreRange => DavRangeSelection::Full,
746        DavRangeLimitBehavior::RangeNotSatisfiable => DavRangeSelection::NotSatisfiable,
747    }
748}
749
750fn coalesce_ranges(
751    ranges: Vec<HttpByteRange>,
752    maximum_gap: u64,
753) -> Result<Vec<HttpByteRange>, DavDownloadPlanError> {
754    let mut coalesced = Vec::with_capacity(ranges.len());
755    for range in ranges {
756        let mut start = range.start();
757        let mut end = range.end();
758        let mut insertion = coalesced.len();
759        let mut index = 0usize;
760        while index < coalesced.len() {
761            let candidate: HttpByteRange = coalesced[index];
762            if ranges_are_close(start, end, candidate.start(), candidate.end(), maximum_gap) {
763                insertion = insertion.min(index);
764                start = start.min(candidate.start());
765                end = end.max(candidate.end());
766                coalesced.remove(index);
767                index = 0;
768            } else {
769                index += 1;
770            }
771        }
772        let merged = HttpByteRange::new(start, end, range.total_size())
773            .map_err(|_| DavDownloadPlanError::InvalidRepresentation)?;
774        if insertion == coalesced.len() {
775            coalesced.push(merged);
776        } else {
777            coalesced.insert(insertion, merged);
778        }
779    }
780    Ok(coalesced)
781}
782
783const fn ranges_are_close(
784    left_start: u64,
785    left_end: u64,
786    right_start: u64,
787    right_end: u64,
788    maximum_gap: u64,
789) -> bool {
790    if left_end < right_start {
791        right_start - left_end - 1 <= maximum_gap
792    } else if right_end < left_start {
793        left_start - right_end - 1 <= maximum_gap
794    } else {
795        true
796    }
797}
798
799fn build_multipart_plan(
800    requested_range_count: usize,
801    selected_length: u64,
802    ranges: Vec<HttpByteRange>,
803    content_type: &str,
804) -> Result<(DavMultipartDownloadPlan, HeaderValue), DavDownloadPlanError> {
805    header_value(content_type)?;
806    let boundary = multipart_boundary();
807    let multipart_content_type =
808        header_value(&format!("multipart/byteranges; boundary={boundary}"))?;
809    let estimated_frame_bytes = ranges
810        .len()
811        .saturating_mul(
812            boundary
813                .len()
814                .saturating_add(content_type.len())
815                .saturating_add(96),
816        )
817        .saturating_add(boundary.len().saturating_add(8));
818    let mut framing = String::with_capacity(estimated_frame_bytes);
819    let mut segments = Vec::with_capacity(ranges.len());
820    for (index, range) in ranges.into_iter().enumerate() {
821        let frame_start = framing.len();
822        if index == 0 {
823            write!(framing, "--{boundary}\r\n")
824        } else {
825            write!(framing, "\r\n--{boundary}\r\n")
826        }
827        .map_err(|_| DavDownloadPlanError::InvalidRepresentation)?;
828        write!(
829            framing,
830            "Content-Type: {content_type}\r\nContent-Range: bytes {}-{}/{}\r\n\r\n",
831            range.start(),
832            range.end(),
833            range.total_size()
834        )
835        .map_err(|_| DavDownloadPlanError::InvalidRepresentation)?;
836        segments.push(DavMultipartSegmentPlan {
837            range,
838            frame_start,
839            frame_end: framing.len(),
840        });
841    }
842    let closing_start = framing.len();
843    write!(framing, "\r\n--{boundary}--\r\n")
844        .map_err(|_| DavDownloadPlanError::InvalidRepresentation)?;
845    let framing = Bytes::from(framing);
846    let framing_length =
847        u64::try_from(framing.len()).map_err(|_| DavDownloadPlanError::InvalidRepresentation)?;
848    let expected_length = selected_length
849        .checked_add(framing_length)
850        .ok_or(DavDownloadPlanError::InvalidRepresentation)?;
851    Ok((
852        DavMultipartDownloadPlan {
853            requested_range_count,
854            selected_length,
855            expected_length,
856            segments,
857            framing,
858            closing_start,
859        },
860        multipart_content_type,
861    ))
862}
863
864fn multipart_boundary() -> String {
865    static SEQUENCE: AtomicU64 = AtomicU64::new(0);
866    let now = SystemTime::now()
867        .duration_since(UNIX_EPOCH)
868        .unwrap_or_default();
869    let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
870    format!(
871        "aster-forge-{:016x}-{:08x}-{sequence:016x}",
872        now.as_secs(),
873        now.subsec_nanos()
874    )
875}
876
877async fn open_multipart_download<Source: DavDownloadSource>(
878    source: &Source,
879    path: &DavPath,
880    plan: DavMultipartDownloadPlan,
881) -> Result<Option<DavOpenedDownload>, DavDownloadOpenError> {
882    let DavMultipartDownloadPlan {
883        expected_length,
884        segments,
885        framing,
886        closing_start,
887        ..
888    } = plan;
889    let mut parts = Vec::with_capacity(segments.len());
890    for segment in segments {
891        let opened = source.open_range(path, segment.range).await?;
892        if opened.expected_length != segment.range.length() {
893            return Err(DavDownloadOpenError::LengthMismatch {
894                planned: segment.range.length(),
895                opened: opened.expected_length,
896            });
897        }
898        parts.push(DavOpenedMultipartPart {
899            stream: opened.stream,
900            expected_length: opened.expected_length,
901            seen: 0,
902            frame_start: segment.frame_start,
903            frame_end: segment.frame_end,
904        });
905    }
906    let stream = DavMultipartStream {
907        parts,
908        framing,
909        closing_start,
910        part_index: 0,
911        phase: DavMultipartStreamPhase::Frame,
912    };
913    Ok(Some(DavOpenedDownload::new(
914        Box::pin(stream),
915        expected_length,
916    )))
917}
918
919struct DavOpenedMultipartPart {
920    stream: DavContentStream,
921    expected_length: u64,
922    seen: u64,
923    frame_start: usize,
924    frame_end: usize,
925}
926
927#[derive(Clone, Copy)]
928enum DavMultipartStreamPhase {
929    Frame,
930    Body,
931    Closing,
932    Done,
933}
934
935struct DavMultipartStream {
936    parts: Vec<DavOpenedMultipartPart>,
937    framing: Bytes,
938    closing_start: usize,
939    part_index: usize,
940    phase: DavMultipartStreamPhase,
941}
942
943impl Stream for DavMultipartStream {
944    type Item = Result<Bytes, DavBackendError>;
945
946    fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
947        let this = self.get_mut();
948        loop {
949            match this.phase {
950                DavMultipartStreamPhase::Frame => {
951                    let Some(part) = this.parts.get(this.part_index) else {
952                        this.phase = DavMultipartStreamPhase::Closing;
953                        continue;
954                    };
955                    let frame = this.framing.slice(part.frame_start..part.frame_end);
956                    this.phase = DavMultipartStreamPhase::Body;
957                    return Poll::Ready(Some(Ok(frame)));
958                }
959                DavMultipartStreamPhase::Body => {
960                    // Frame enters Body only after it confirmed this index exists.
961                    let part = &mut this.parts[this.part_index];
962                    match part.stream.as_mut().poll_next(context) {
963                        Poll::Pending => return Poll::Pending,
964                        Poll::Ready(Some(Ok(chunk))) => {
965                            if chunk.is_empty() {
966                                continue;
967                            }
968                            let chunk_length = chunk.len() as u64;
969                            if chunk_length > part.expected_length - part.seen {
970                                this.phase = DavMultipartStreamPhase::Done;
971                                return Poll::Ready(Some(Err(multipart_stream_error())));
972                            }
973                            part.seen += chunk_length;
974                            return Poll::Ready(Some(Ok(chunk)));
975                        }
976                        Poll::Ready(Some(Err(error))) => {
977                            this.phase = DavMultipartStreamPhase::Done;
978                            return Poll::Ready(Some(Err(error)));
979                        }
980                        Poll::Ready(None) if part.seen == part.expected_length => {
981                            this.part_index += 1;
982                            this.phase = DavMultipartStreamPhase::Frame;
983                        }
984                        Poll::Ready(None) => {
985                            this.phase = DavMultipartStreamPhase::Done;
986                            return Poll::Ready(Some(Err(multipart_stream_error())));
987                        }
988                    }
989                }
990                DavMultipartStreamPhase::Closing => {
991                    this.phase = DavMultipartStreamPhase::Done;
992                    return Poll::Ready(Some(Ok(this.framing.slice(this.closing_start..))));
993                }
994                DavMultipartStreamPhase::Done => return Poll::Ready(None),
995            }
996        }
997    }
998}
999
1000const fn multipart_stream_error() -> DavBackendError {
1001    DavBackendError::new(DavBackendErrorKind::Internal)
1002}
1003
1004fn if_range_matches(headers: &HeaderMap, etag: Option<&str>, last_modified: SystemTime) -> bool {
1005    let Some(value) = headers.get(IF_RANGE) else {
1006        return true;
1007    };
1008    let Ok(raw) = value.to_str() else {
1009        return false;
1010    };
1011    let raw = raw.trim();
1012    if raw.starts_with('"')
1013        || raw
1014            .get(..2)
1015            .is_some_and(|prefix| prefix.eq_ignore_ascii_case("W/"))
1016    {
1017        return strong_if_range_etag_matches(raw, etag);
1018    }
1019    parse_http_date(raw)
1020        .is_ok_and(|date| http_date_epoch_seconds(date) == http_date_epoch_seconds(last_modified))
1021}
1022
1023fn strong_if_range_etag_matches(candidate: &str, current: Option<&str>) -> bool {
1024    if candidate
1025        .get(..2)
1026        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("W/"))
1027    {
1028        return false;
1029    }
1030    let Some(candidate) = candidate
1031        .strip_prefix('"')
1032        .and_then(|value| value.strip_suffix('"'))
1033        .filter(|value| !value.contains('"'))
1034    else {
1035        return false;
1036    };
1037    let Some(current) = current else {
1038        return false;
1039    };
1040    if current
1041        .trim()
1042        .get(..2)
1043        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("W/"))
1044    {
1045        return false;
1046    }
1047    let current = current.trim();
1048    let current = current
1049        .strip_prefix('"')
1050        .and_then(|value| value.strip_suffix('"'))
1051        .unwrap_or(current);
1052    candidate == current
1053}
1054
1055/// Builds the response required when a byte range cannot be served.
1056#[must_use]
1057pub fn range_not_satisfiable_response(content_length: u64) -> DavResponse {
1058    let mut response = DavResponse::empty(StatusCode::RANGE_NOT_SATISFIABLE);
1059    response.headers.insert(
1060        CONTENT_RANGE,
1061        HeaderValue::from_str(&format!("bytes */{content_length}"))
1062            .unwrap_or_else(|_| HeaderValue::from_static("bytes */0")),
1063    );
1064    response
1065        .headers
1066        .insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
1067    response
1068        .headers
1069        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
1070    response
1071}
1072
1073fn range_not_satisfiable_plan(
1074    content_length: u64,
1075    conditional: &DavConditionalPlan,
1076) -> DavDownloadPlan {
1077    let mut response = range_not_satisfiable_response(content_length);
1078    conditional.apply_response_headers(response.status, &mut response.headers);
1079    DavDownloadPlan {
1080        response,
1081        body: DavDownloadBody::Empty,
1082    }
1083}
1084
1085fn header_value(value: &str) -> Result<HeaderValue, DavDownloadPlanError> {
1086    HeaderValue::from_str(value).map_err(|_| DavDownloadPlanError::InvalidRepresentation)
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use http::HeaderValue;
1092    use http::StatusCode;
1093    use http::header::{CACHE_CONTROL, CONTENT_TYPE};
1094
1095    use super::{DavResponseBody, text_document_response};
1096
1097    #[test]
1098    fn text_document_response_has_explicit_success_and_server_error_cache_policy() {
1099        let response = text_document_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal error");
1100
1101        assert_eq!(
1102            response.headers.get(CACHE_CONTROL),
1103            Some(&HeaderValue::from_static("no-store"))
1104        );
1105        assert_eq!(
1106            response.headers.get(CONTENT_TYPE),
1107            Some(&HeaderValue::from_static("text/plain; charset=utf-8"))
1108        );
1109        assert!(matches!(
1110            response.body,
1111            DavResponseBody::Bytes(ref body) if body == "Internal error"
1112        ));
1113
1114        let success = text_document_response(StatusCode::OK, "OK");
1115        assert!(success.headers.get(CACHE_CONTROL).is_none());
1116    }
1117}