1use 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum DavDownloadBody {
44 Empty,
46 Full { expected_length: u64 },
48 Range(HttpByteRange),
50 Multipart(DavMultipartDownloadPlan),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct DavMultiRangeLimits {
57 pub maximum_header_bytes: usize,
58 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum DavRangeLimitBehavior {
91 IgnoreRange,
93 RangeNotSatisfiable,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct DavMultiRangePolicy {
100 pub limits: DavMultiRangeLimits,
101 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#[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#[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 #[must_use]
150 pub const fn requested_range_count(&self) -> usize {
151 self.requested_range_count
152 }
153
154 #[must_use]
156 pub fn segments(&self) -> &[DavMultipartSegmentPlan] {
157 &self.segments
158 }
159
160 #[must_use]
162 pub const fn selected_length(&self) -> u64 {
163 self.selected_length
164 }
165
166 #[must_use]
168 pub const fn expected_length(&self) -> u64 {
169 self.expected_length
170 }
171}
172
173pub struct DavDownloadPlan {
175 pub response: DavResponse,
176 pub body: DavDownloadBody,
177}
178
179#[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
188pub enum DavResponseBody {
190 Empty,
191 Bytes(Bytes),
192 Stream(DavContentStream),
193 MultiStatus(DavMultiStatusStream),
194}
195
196pub struct DavResponse {
198 pub status: StatusCode,
199 pub headers: HeaderMap,
200 pub body: DavResponseBody,
201}
202
203impl DavResponse {
204 #[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 #[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#[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#[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#[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#[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#[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#[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
375pub 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#[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
417pub 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
441pub 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
609pub 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 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 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#[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}