Skip to content

Commit fc9324e

Browse files
Handle quoted directives in Cache-Control header
This fixes a parsing bug that resulted in commas within quoted blocks being treated as delimiters for directives.
1 parent 6a923d6 commit fc9324e

File tree

5 files changed

+52
-35
lines changed

5 files changed

+52
-35
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ bytes = "1"
2323
mime = "0.3.14"
2424
sha1 = "0.10"
2525
httpdate = "1"
26+
either = "1.6.0"
2627

2728
[features]
2829
nightly = []

src/common/cache_control.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use std::iter::FromIterator;
33
use std::str::FromStr;
44
use std::time::Duration;
55

6+
use either::Either;
67
use http::{HeaderName, HeaderValue};
78

8-
use crate::util::{self, csv, Seconds};
9+
use crate::util::{self, csv, iter_flat_csv, Seconds};
910
use crate::{Error, Header};
1011

1112
/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
@@ -241,7 +242,17 @@ impl Header for CacheControl {
241242
}
242243

243244
fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
244-
csv::from_comma_delimited(values).map(|FromIter(cc)| cc)
245+
let directives = values.flat_map(|value| {
246+
let Ok(value) = value.to_str() else {
247+
return Either::Left(std::iter::once(Err(())));
248+
};
249+
Either::Right(iter_flat_csv(value, ',').map(KnownDirective::from_str))
250+
});
251+
252+
match directives.collect() {
253+
Ok(FromIter(cc)) => Ok(cc),
254+
Err(()) => Err(Error::invalid()),
255+
}
245256
}
246257

247258
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
@@ -489,12 +500,7 @@ mod tests {
489500
fn test_parse_quoted_comma() {
490501
assert_eq!(
491502
test_decode::<CacheControl>(&["foo=\"a, private, immutable, b\", no-cache"]).unwrap(),
492-
CacheControl::new()
493-
.with_no_cache()
494-
// These are wrong: these appear inside a quoted string and so
495-
// should be treated as parameter for the "a" directive.
496-
.with_private()
497-
.with_immutable(),
503+
CacheControl::new().with_no_cache(),
498504
"unknown extensions are ignored but shouldn't fail parsing",
499505
)
500506
}

src/util/csv.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use http::HeaderValue;
55
use crate::Error;
66

77
/// Reads a comma-delimited raw header into a Vec.
8+
#[allow(unused)]
89
pub(crate) fn from_comma_delimited<'i, I, T, E>(values: &mut I) -> Result<E, Error>
910
where
1011
I: Iterator<Item = &'i HeaderValue>,

src/util/flat_csv.rs

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,11 @@ impl Separator for SemiColon {
3838

3939
impl<Sep: Separator> FlatCsv<Sep> {
4040
pub(crate) fn iter(&self) -> impl Iterator<Item = &str> {
41-
self.value.to_str().ok().into_iter().flat_map(|value_str| {
42-
let mut in_quotes = false;
43-
value_str
44-
.split(move |c| {
45-
#[allow(clippy::collapsible_else_if)]
46-
if in_quotes {
47-
if c == '"' {
48-
in_quotes = false;
49-
}
50-
false // dont split
51-
} else {
52-
if c == Sep::CHAR {
53-
true // split
54-
} else {
55-
if c == '"' {
56-
in_quotes = true;
57-
}
58-
false // dont split
59-
}
60-
}
61-
})
62-
.map(|item| item.trim())
63-
})
41+
self.value
42+
.to_str()
43+
.ok()
44+
.into_iter()
45+
.flat_map(|value_str| iter_flat_csv(value_str, Sep::CHAR))
6446
}
6547
}
6648

@@ -161,6 +143,34 @@ impl<Sep: Separator> FromIterator<HeaderValue> for FlatCsv<Sep> {
161143
}
162144
}
163145

146+
/// Iterate over comma-delimited values, preserving quoted groups.
147+
///
148+
/// The returned iterator produces non-overlapping string slices split at
149+
/// `separator`, except when `separator` occurs within quotes.
150+
pub(crate) fn iter_flat_csv(value: &str, separator: char) -> impl Iterator<Item = &str> {
151+
let mut in_quotes = false;
152+
value
153+
.split(move |c| {
154+
#[allow(clippy::collapsible_else_if)]
155+
if in_quotes {
156+
if c == '"' {
157+
in_quotes = false;
158+
}
159+
false // dont split
160+
} else {
161+
if c == separator {
162+
true // split
163+
} else {
164+
if c == '"' {
165+
in_quotes = true;
166+
}
167+
false // dont split
168+
}
169+
}
170+
})
171+
.map(|item| item.trim())
172+
}
173+
164174
#[cfg(test)]
165175
mod tests {
166176
use super::*;
@@ -191,10 +201,9 @@ mod tests {
191201

192202
#[test]
193203
fn quoted_text() {
194-
let val = HeaderValue::from_static("foo=\"bar,baz\", sherlock=holmes");
195-
let csv = FlatCsv::<Comma>::from(val);
204+
let val = "foo=\"bar,baz\", sherlock=holmes";
196205

197-
let mut values = csv.iter();
206+
let mut values = iter_flat_csv(val, ',');
198207
assert_eq!(values.next(), Some("foo=\"bar,baz\""));
199208
assert_eq!(values.next(), Some("sherlock=holmes"));
200209
assert_eq!(values.next(), None);

src/util/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::Error;
55
//pub use self::charset::Charset;
66
//pub use self::encoding::Encoding;
77
pub(crate) use self::entity::{EntityTag, EntityTagRange};
8-
pub(crate) use self::flat_csv::{FlatCsv, SemiColon};
8+
pub(crate) use self::flat_csv::{iter_flat_csv, FlatCsv, SemiColon};
99
pub(crate) use self::fmt::fmt;
1010
pub(crate) use self::http_date::HttpDate;
1111
pub(crate) use self::iter::IterExt;

0 commit comments

Comments
 (0)