base_web_multipart/
content_disposition.rs1use crate::ParameterRules;
5use crate::syntax::{
6 PARAMETER_COUNT_LIMIT, field_text, is_field_value_byte, is_token, lowercase,
7 parse_parameterized, write_parameter_value,
8};
9use base_message::{FieldType, FieldValueError, combination::Singleton};
10use std::borrow::Cow;
11use std::fmt;
12
13#[derive(Clone, PartialEq, Eq, Hash)]
33pub struct ContentDisposition<'a> {
34 disposition_type: Cow<'a, str>,
35 parameters: Vec<(Cow<'a, str>, Cow<'a, str>)>,
36}
37
38impl ContentDisposition<'static> {
39 pub fn new(disposition_type: &str) -> Result<Self, FieldValueError> {
45 if !is_token(disposition_type) {
46 return Err(FieldValueError::new("disposition type is not a token"));
47 }
48 Ok(ContentDisposition {
49 disposition_type: Cow::Owned(disposition_type.to_ascii_lowercase()),
50 parameters: Vec::new(),
51 })
52 }
53
54 pub fn form_data(name: &str) -> Result<Self, FieldValueError> {
60 ContentDisposition {
61 disposition_type: Cow::Borrowed("form-data"),
62 parameters: Vec::new(),
63 }
64 .with_parameter("name", name)
65 }
66}
67
68impl<'a> ContentDisposition<'a> {
69 pub fn with_parameter(
75 mut self,
76 name: &str,
77 value: &str,
78 ) -> Result<ContentDisposition<'a>, FieldValueError> {
79 if !is_token(name) {
80 return Err(FieldValueError::new("parameter name is not a token"));
81 }
82 if !value.bytes().all(is_field_value_byte) {
83 return Err(FieldValueError::new(
84 "parameter value contains a control character",
85 ));
86 }
87 if name.ends_with('*') && !is_token(value) {
88 return Err(FieldValueError::new(
89 "extended parameter value is not a token",
90 ));
91 }
92 let name = lowercase(name).into_owned();
93 if self.parameter(&name).is_some() {
94 return Err(FieldValueError::new("duplicate parameter"));
95 }
96 if self.parameters.len() == PARAMETER_COUNT_LIMIT {
97 return Err(FieldValueError::new("too many parameters"));
98 }
99 self.parameters
100 .push((Cow::Owned(name), Cow::Owned(value.to_owned())));
101 Ok(self)
102 }
103
104 pub fn disposition_type(&self) -> &str {
106 &self.disposition_type
107 }
108
109 pub fn parameter(&self, name: &str) -> Option<&str> {
111 self.parameters
112 .iter()
113 .find(|(parameter, _)| parameter.eq_ignore_ascii_case(name))
114 .map(|(_, value)| &**value)
115 }
116
117 pub fn parameters(&self) -> impl ExactSizeIterator<Item = (&str, &str)> {
119 self.parameters
120 .iter()
121 .map(|(name, value)| (&**name, &**value))
122 }
123
124 pub fn name(&self, rules: ParameterRules) -> Result<Option<Cow<'_, str>>, FieldValueError> {
130 self.resolve("name", rules)
131 }
132
133 pub fn filename(&self, rules: ParameterRules) -> Result<Option<Cow<'_, str>>, FieldValueError> {
139 self.resolve("filename", rules)
140 }
141
142 fn resolve(
144 &self,
145 name: &str,
146 rules: ParameterRules,
147 ) -> Result<Option<Cow<'_, str>>, FieldValueError> {
148 match rules {
149 ParameterRules::FormData => {
150 if self
152 .parameters
153 .iter()
154 .any(|(parameter, _)| parameter.contains('*'))
155 {
156 return Err(FieldValueError::new(
157 "extended parameters are not allowed in form data",
158 ));
159 }
160 }
161 ParameterRules::Mime => {
162 let mut extended = None;
163 for (parameter, value) in &self.parameters {
164 match parameter
165 .strip_prefix(name)
166 .and_then(|rest| rest.strip_prefix('*'))
167 {
168 Some("") => extended = Some(value),
169 Some(_) => {
171 return Err(FieldValueError::new(
172 "parameter continuations are not supported",
173 ));
174 }
175 None => {}
176 }
177 }
178 if let Some(value) = extended {
179 return decode_extended_value(value).map(Some);
180 }
181 }
182 }
183 Ok(self.parameter(name).map(Cow::Borrowed))
184 }
185
186 pub fn into_owned(self) -> ContentDisposition<'static> {
188 let owned = |text: Cow<'a, str>| Cow::Owned(text.into_owned());
189 ContentDisposition {
190 disposition_type: owned(self.disposition_type),
191 parameters: self
192 .parameters
193 .into_iter()
194 .map(|(name, value)| (owned(name), owned(value)))
195 .collect(),
196 }
197 }
198}
199
200impl<'a> FieldType<'a> for ContentDisposition<'a> {
201 type Combination = Singleton;
202
203 fn parse(value: &'a [u8]) -> Result<Self, FieldValueError> {
204 let (disposition_type, parameters) = parse_parameterized(field_text(value)?)?;
205 Ok(Self {
206 disposition_type,
207 parameters,
208 })
209 }
210}
211
212impl fmt::Display for ContentDisposition<'_> {
214 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215 let mut text = String::from(&*self.disposition_type);
216 for (name, value) in &self.parameters {
217 text.push_str("; ");
218 text.push_str(name);
219 text.push('=');
220 write_parameter_value(&mut text, name, value);
221 }
222 formatter.write_str(&text)
223 }
224}
225
226impl fmt::Debug for ContentDisposition<'_> {
228 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229 let parameters: Vec<_> = self
230 .parameters
231 .iter()
232 .map(|(name, value)| (&**name, format!("<{} bytes>", value.len())))
233 .collect();
234 formatter
235 .debug_struct("ContentDisposition")
236 .field("disposition_type", &self.disposition_type)
237 .field("parameters", ¶meters)
238 .finish()
239 }
240}
241
242fn decode_extended_value(value: &str) -> Result<Cow<'_, str>, FieldValueError> {
244 let invalid = |reason| Err(FieldValueError::new(reason));
245 let Some((charset, Some((_language, encoded)))) = value
246 .split_once('\'')
247 .map(|(charset, rest)| (charset, rest.split_once('\'')))
248 else {
249 return invalid("extended parameter value has no character set");
250 };
251 let ascii = if charset.eq_ignore_ascii_case("us-ascii") {
252 true
253 } else if charset.eq_ignore_ascii_case("utf-8") {
254 false
255 } else {
256 return invalid("extended parameter value has an unsupported character set");
257 };
258 let mut bytes = Vec::with_capacity(encoded.len());
259 let mut input = encoded.bytes();
260 while let Some(byte) = input.next() {
261 if byte != b'%' {
262 bytes.push(byte);
263 continue;
264 }
265 let digit = |byte: Option<u8>| byte.and_then(|byte| char::from(byte).to_digit(16));
266 match (digit(input.next()), digit(input.next())) {
267 (Some(high), Some(low)) => bytes.push((high * 16 + low) as u8),
268 _ => return invalid("extended parameter value has an invalid percent-encoding"),
269 }
270 }
271 if ascii && !bytes.is_ascii() {
272 return invalid("extended parameter value is not US-ASCII");
273 }
274 if bytes == encoded.as_bytes() {
275 return Ok(Cow::Borrowed(encoded));
276 }
277 String::from_utf8(bytes)
278 .map(Cow::Owned)
279 .map_err(|_| FieldValueError::new("extended parameter value is not UTF-8"))
280}