aster_forge_webdav/
patch.rs1use aster_forge_utils::http_validators;
4use headers::{ContentType, Header, Mime};
5use http::header::{CACHE_CONTROL, CONTENT_TYPE};
6use http::{HeaderMap, HeaderValue, StatusCode};
7
8use crate::response::no_store_empty_response;
9use crate::{
10 DavBodyPolicy, DavCapabilitySnapshot, DavConditionalOutcome, DavConditionalPlan,
11 DavConditionalPlanError, DavConditionalResource, DavMethod, DavPatchBodyPolicy, DavPatchFormat,
12 DavProtocolError, DavResponse, DavWritePrecondition, method_not_allowed_response,
13 plan_http_conditionals,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct DavPatchPlan {
19 pub format: &'static DavPatchFormat,
20 pub body_policy: DavBodyPolicy,
21 pub resource_existed: bool,
22}
23
24#[derive(Debug, thiserror::Error)]
26pub enum DavPatchPlanError {
27 #[error("PATCH is not allowed for this resource")]
28 MethodNotAllowed,
29 #[error("PATCH Content-Type is not supported for this resource")]
30 UnsupportedMediaType,
31 #[error("PATCH requires If-Match with a strong entity-tag")]
32 PreconditionRequired,
33 #[error(transparent)]
34 Protocol(#[from] DavProtocolError),
35 #[error("PATCH request precondition failed")]
36 PreconditionFailed(DavConditionalPlan),
37 #[error("invalid PATCH representation metadata")]
38 InvalidRepresentation,
39}
40
41pub fn plan_patch_request(
47 snapshot: &DavCapabilitySnapshot,
48 headers: &HeaderMap,
49 resource: DavConditionalResource<'_>,
50) -> Result<DavPatchPlan, DavPatchPlanError> {
51 let Some(formats) = snapshot.patch_formats() else {
52 return Err(DavPatchPlanError::MethodNotAllowed);
53 };
54 let content_type = parse_content_type(headers)?;
55 let format = formats
56 .iter()
57 .find(|format| {
58 format
59 .media_type
60 .parse::<Mime>()
61 .is_ok_and(|supported| supported == content_type)
62 })
63 .ok_or(DavPatchPlanError::UnsupportedMediaType)?;
64
65 enforce_write_precondition(format.precondition, headers).map_err(|error| match error {
66 DavWritePreconditionError::Required => DavPatchPlanError::PreconditionRequired,
67 DavWritePreconditionError::Protocol(error) => DavPatchPlanError::Protocol(error),
68 })?;
69 let conditional = plan_http_conditionals(DavMethod::Patch, headers, resource).map_err(
70 |error| match error {
71 DavConditionalPlanError::Protocol(error) => DavPatchPlanError::Protocol(error),
72 DavConditionalPlanError::InvalidRepresentation => {
73 DavPatchPlanError::InvalidRepresentation
74 }
75 },
76 )?;
77 if conditional.outcome != DavConditionalOutcome::Proceed {
78 return Err(DavPatchPlanError::PreconditionFailed(conditional));
79 }
80
81 let body_policy = match format.body_policy {
82 DavPatchBodyPolicy::Bounded { maximum } => DavBodyPolicy::Bounded { maximum },
83 DavPatchBodyPolicy::Stream => DavBodyPolicy::Stream,
84 };
85 Ok(DavPatchPlan {
86 format,
87 body_policy,
88 resource_existed: resource.exists,
89 })
90}
91
92#[must_use]
94pub fn patch_plan_error_response(
95 error: &DavPatchPlanError,
96 snapshot: &DavCapabilitySnapshot,
97) -> DavResponse {
98 match error {
99 DavPatchPlanError::MethodNotAllowed => method_not_allowed_response(snapshot),
100 DavPatchPlanError::UnsupportedMediaType => {
101 let mut response = no_store_empty_response(StatusCode::UNSUPPORTED_MEDIA_TYPE);
102 if let Some(accept_patch) = snapshot.accept_patch_header() {
103 response
104 .headers
105 .insert("Accept-Patch", accept_patch.clone());
106 }
107 response
108 }
109 DavPatchPlanError::PreconditionRequired => {
110 no_store_empty_response(StatusCode::PRECONDITION_REQUIRED)
111 }
112 DavPatchPlanError::Protocol(error) => crate::protocol_error_response(error),
113 DavPatchPlanError::PreconditionFailed(plan) => {
114 let mut response = DavResponse::empty(StatusCode::PRECONDITION_FAILED);
115 response
116 .headers
117 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
118 plan.apply_response_headers(response.status, &mut response.headers);
119 response
120 }
121 DavPatchPlanError::InvalidRepresentation => {
122 no_store_empty_response(StatusCode::INTERNAL_SERVER_ERROR)
123 }
124 }
125}
126
127pub(crate) enum DavWritePreconditionError {
128 Required,
129 Protocol(DavProtocolError),
130}
131
132pub(crate) fn enforce_write_precondition(
133 policy: DavWritePrecondition,
134 headers: &HeaderMap,
135) -> Result<(), DavWritePreconditionError> {
136 if policy == DavWritePrecondition::Optional {
137 return Ok(());
138 }
139 match http_validators::if_match_headers_have_strong_tag(headers) {
140 Ok(Some(true)) => Ok(()),
141 Ok(Some(false) | None) => Err(DavWritePreconditionError::Required),
142 Err(_) => Err(DavWritePreconditionError::Protocol(
143 DavProtocolError::bad_request("Invalid If-Match header"),
144 )),
145 }
146}
147
148fn parse_content_type(headers: &HeaderMap) -> Result<Mime, DavPatchPlanError> {
149 let mut values = headers.get_all(CONTENT_TYPE).iter();
150 let Some(value) = values.next() else {
151 return Err(DavPatchPlanError::UnsupportedMediaType);
152 };
153 if values.next().is_some() {
154 return Err(DavPatchPlanError::UnsupportedMediaType);
155 }
156 let content_type = ContentType::decode(&mut std::iter::once(value))
157 .map_err(|_| DavPatchPlanError::UnsupportedMediaType)?;
158 Ok(content_type.into())
159}