-
-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft: RFC configuration in const context & HTTP-like protocol parsing support #129
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -239,21 +239,66 @@ impl<T> Status<T> { | |
} | ||
|
||
/// Parser configuration. | ||
#[derive(Clone, Debug, Default)] | ||
#[derive(Clone)] | ||
pub struct ParserConfig { | ||
allow_spaces_after_header_name_in_responses: bool, | ||
allow_obsolete_multiline_headers_in_responses: bool, | ||
allow_multiple_spaces_in_request_line_delimiters: bool, | ||
allow_multiple_spaces_in_response_status_delimiters: bool, | ||
ignore_invalid_headers_in_responses: bool, | ||
|
||
version_parser: fn(&mut Bytes) -> Result<u8>, | ||
} | ||
|
||
impl fmt::Debug for ParserConfig { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
f.debug_struct("ParserConfig") | ||
.field("allow_spaces_after_header_name_in_responses", &self.allow_spaces_after_header_name_in_responses) | ||
.field("allow_obsolete_multiline_headers_in_responses", &self.allow_obsolete_multiline_headers_in_responses) | ||
.field("allow_multiple_spaces_in_request_line_delimiters", &self.allow_multiple_spaces_in_request_line_delimiters) | ||
.field("allow_multiple_spaces_in_response_status_delimiters", &self.ignore_invalid_headers_in_responses) | ||
.field("ignore_invalid_headers_in_responses", &self.ignore_invalid_headers_in_responses) | ||
.finish() | ||
} | ||
} | ||
|
||
impl Default for ParserConfig { | ||
fn default() -> Self { | ||
Self::const_default() | ||
} | ||
} | ||
|
||
impl ParserConfig { | ||
/// Returns default configuration. Same as `Default::default()` but can be used in const | ||
/// context. | ||
pub const fn const_default() -> Self { | ||
Self { | ||
allow_spaces_after_header_name_in_responses: false, | ||
allow_obsolete_multiline_headers_in_responses: false, | ||
allow_multiple_spaces_in_request_line_delimiters: false, | ||
allow_multiple_spaces_in_response_status_delimiters: false, | ||
ignore_invalid_headers_in_responses: false, | ||
version_parser: HTTP_VERSION_PARSER, | ||
} | ||
} | ||
|
||
/// Sets first-line parsing to SIP | ||
pub const fn set_sip_protocol_parser(mut self) -> Self { | ||
self.version_parser = SIP_VERSION_PARSER; | ||
self | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would love to see this as something like the following: pub const fn set_protocol_parser(mut self, protocol: Protocol) -> Self {
self.version_parser = match protocol {
Protocol::Http => HTTP_VERSION_PARSER,
Protocol::Sip => SIP_VERSION_PARSER,
};
self
} However, that gives the following compilation error:
which is weird :| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean, is it only the attribute type, or the |
||
} | ||
|
||
/// Set first-line parsing to HTTP | ||
pub const fn set_http_protocol_parser(mut self) -> Self { | ||
self.version_parser = HTTP_VERSION_PARSER; | ||
self | ||
} | ||
|
||
/// Sets whether spaces and tabs should be allowed after header names in responses. | ||
pub fn allow_spaces_after_header_name_in_responses( | ||
&mut self, | ||
pub const fn allow_spaces_after_header_name_in_responses( | ||
mut self, | ||
value: bool, | ||
) -> &mut Self { | ||
) -> Self { | ||
self.allow_spaces_after_header_name_in_responses = value; | ||
self | ||
} | ||
|
@@ -271,13 +316,13 @@ impl ParserConfig { | |
/// request line to contain the other mentioned whitespace characters. | ||
/// | ||
/// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.3.p.3 | ||
pub fn allow_multiple_spaces_in_request_line_delimiters(&mut self, value: bool) -> &mut Self { | ||
pub const fn allow_multiple_spaces_in_request_line_delimiters(mut self, value: bool) -> Self { | ||
self.allow_multiple_spaces_in_request_line_delimiters = value; | ||
self | ||
} | ||
|
||
/// Whether multiple spaces are allowed as delimiters in request lines. | ||
pub fn multiple_spaces_in_request_line_delimiters_are_allowed(&self) -> bool { | ||
pub const fn multiple_spaces_in_request_line_delimiters_are_allowed(&self) -> bool { | ||
self.allow_multiple_spaces_in_request_line_delimiters | ||
} | ||
|
||
|
@@ -295,13 +340,13 @@ impl ParserConfig { | |
/// line to contain the other mentioned whitespace characters. | ||
/// | ||
/// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.3 | ||
pub fn allow_multiple_spaces_in_response_status_delimiters(&mut self, value: bool) -> &mut Self { | ||
pub const fn allow_multiple_spaces_in_response_status_delimiters(mut self, value: bool) -> Self { | ||
self.allow_multiple_spaces_in_response_status_delimiters = value; | ||
self | ||
} | ||
|
||
/// Whether multiple spaces are allowed as delimiters in response status lines. | ||
pub fn multiple_spaces_in_response_status_delimiters_are_allowed(&self) -> bool { | ||
pub const fn multiple_spaces_in_response_status_delimiters_are_allowed(&self) -> bool { | ||
self.allow_multiple_spaces_in_response_status_delimiters | ||
} | ||
|
||
|
@@ -328,16 +373,16 @@ impl ParserConfig { | |
/// assert_eq!(response.headers[0].name, "Folded-Header"); | ||
/// assert_eq!(response.headers[0].value, b"hello\r\n there"); | ||
/// ``` | ||
pub fn allow_obsolete_multiline_headers_in_responses( | ||
&mut self, | ||
pub const fn allow_obsolete_multiline_headers_in_responses( | ||
mut self, | ||
value: bool, | ||
) -> &mut Self { | ||
) -> Self { | ||
self.allow_obsolete_multiline_headers_in_responses = value; | ||
self | ||
} | ||
|
||
/// Whether obsolete multiline headers should be allowed. | ||
pub fn obsolete_multiline_headers_in_responses_are_allowed(&self) -> bool { | ||
pub const fn obsolete_multiline_headers_in_responses_are_allowed(&self) -> bool { | ||
self.allow_obsolete_multiline_headers_in_responses | ||
} | ||
|
||
|
@@ -390,10 +435,10 @@ impl ParserConfig { | |
/// with whitespace, those will be ignored too. An error will be emitted | ||
/// nonetheless if it finds `\0` or a lone `\r` while looking for the | ||
/// next line. | ||
pub fn ignore_invalid_headers_in_responses( | ||
&mut self, | ||
pub const fn ignore_invalid_headers_in_responses( | ||
mut self, | ||
value: bool, | ||
) -> &mut Self { | ||
) -> Self { | ||
self.ignore_invalid_headers_in_responses = value; | ||
self | ||
} | ||
|
@@ -501,14 +546,14 @@ impl<'h, 'b> Request<'h, 'b> { | |
if config.allow_multiple_spaces_in_request_line_delimiters { | ||
complete!(skip_spaces(&mut bytes)); | ||
} | ||
self.version = Some(complete!(parse_version(&mut bytes))); | ||
self.version = Some(complete!((config.version_parser)(&mut bytes))); | ||
newline!(bytes); | ||
|
||
let len = orig_len - bytes.len(); | ||
let headers_len = complete!(parse_headers_iter_uninit( | ||
&mut headers, | ||
&mut bytes, | ||
&ParserConfig::default(), | ||
&ParserConfig::const_default(), | ||
)); | ||
/* SAFETY: see `parse_headers_iter_uninit` guarantees */ | ||
self.headers = unsafe { assume_init_slice(headers) }; | ||
|
@@ -626,7 +671,7 @@ impl<'h, 'b> Response<'h, 'b> { | |
|
||
/// Try to parse a buffer of bytes into this `Response`. | ||
pub fn parse(&mut self, buf: &'b [u8]) -> Result<usize> { | ||
self.parse_with_config(buf, &ParserConfig::default()) | ||
self.parse_with_config(buf, &ParserConfig::const_default()) | ||
} | ||
|
||
fn parse_with_config(&mut self, buf: &'b [u8], config: &ParserConfig) -> Result<usize> { | ||
|
@@ -656,7 +701,7 @@ impl<'h, 'b> Response<'h, 'b> { | |
let mut bytes = Bytes::new(buf); | ||
|
||
complete!(skip_empty_lines(&mut bytes)); | ||
self.version = Some(complete!(parse_version(&mut bytes))); | ||
self.version = Some(complete!((config.version_parser)(&mut bytes))); | ||
space!(bytes or Error::Version); | ||
if config.allow_multiple_spaces_in_response_status_delimiters { | ||
complete!(skip_spaces(&mut bytes)); | ||
|
@@ -742,8 +787,10 @@ impl<'a> fmt::Debug for Header<'a> { | |
/// ``` | ||
pub const EMPTY_HEADER: Header<'static> = Header { name: "", value: b"" }; | ||
|
||
const HTTP_VERSION_PARSER: fn(&mut Bytes) -> Result<u8> = parse_version_http; | ||
|
||
#[inline] | ||
fn parse_version(bytes: &mut Bytes<'_>) -> Result<u8> { | ||
fn parse_version_http(bytes: &mut Bytes) -> Result<u8> { | ||
if let Some(eight) = bytes.peek_n::<[u8; 8]>(8) { | ||
unsafe { bytes.advance(8); } | ||
return match &eight { | ||
|
@@ -767,6 +814,33 @@ fn parse_version(bytes: &mut Bytes<'_>) -> Result<u8> { | |
Ok(Status::Partial) | ||
} | ||
|
||
const SIP_VERSION_PARSER: fn(&mut Bytes) -> Result<u8> = parse_version_sip; | ||
|
||
#[inline] | ||
fn parse_version_sip(bytes: &mut Bytes) -> Result<u8> { | ||
const PEEK_BY: usize = 7; | ||
|
||
if let Some(seven) = bytes.peek_n::<[u8; PEEK_BY]>(PEEK_BY) { | ||
unsafe { bytes.advance(PEEK_BY); } | ||
return match &seven { | ||
b"SIP/2.0" => Ok(Status::Complete(0)), | ||
_ => Err(Error::Version), | ||
} | ||
} | ||
|
||
// else (but not in `else` because of borrow checker) | ||
|
||
// If there aren't at least 8 bytes, we still want to detect early | ||
// if this is a valid version or not. If it is, we'll return Partial. | ||
expect!(bytes.next() == b'S' => Err(Error::Version)); | ||
expect!(bytes.next() == b'I' => Err(Error::Version)); | ||
expect!(bytes.next() == b'P' => Err(Error::Version)); | ||
expect!(bytes.next() == b'/' => Err(Error::Version)); | ||
expect!(bytes.next() == b'2' => Err(Error::Version)); | ||
expect!(bytes.next() == b'.' => Err(Error::Version)); | ||
Ok(Status::Partial) | ||
} | ||
|
||
/// From [RFC 7230](https://tools.ietf.org/html/rfc7230): | ||
/// | ||
/// > ```notrust | ||
|
@@ -2233,4 +2307,35 @@ mod tests { | |
assert_eq!(response.headers[0].name, "Bread"); | ||
assert_eq!(response.headers[0].value, &b"baguette"[..]); | ||
} | ||
|
||
#[test] | ||
fn test_config_in_const_ctx() { | ||
const CONFIG: super::ParserConfig = super::ParserConfig::const_default() | ||
.allow_spaces_after_header_name_in_responses(true) | ||
.allow_multiple_spaces_in_request_line_delimiters(true); | ||
|
||
assert!(CONFIG.allow_spaces_after_header_name_in_responses); | ||
assert!(CONFIG.allow_multiple_spaces_in_request_line_delimiters); | ||
} | ||
|
||
#[test] | ||
fn test_sip_resp() { | ||
const RESPONSE: &[u8] = | ||
b"SIP/2.0 200 OK\r\nVia: SIP/2.0/UDP example.com;branch=FFFFFFFFFFFFFF;received=10.10.10.10\r\n\r\n"; | ||
|
||
let mut headers = [EMPTY_HEADER; 1]; | ||
let mut response = Response::new(&mut headers[..]); | ||
|
||
let result = crate::ParserConfig::const_default() | ||
.set_sip_protocol_parser() | ||
.parse_response(&mut response, RESPONSE); | ||
assert_eq!(result, Ok(Status::Complete(91))); | ||
|
||
assert_eq!(response.version.unwrap(), 0); | ||
assert_eq!(response.code.unwrap(), 200); | ||
assert_eq!(response.reason.unwrap(), "OK"); | ||
assert_eq!(response.headers.len(), 1); | ||
assert_eq!(response.headers[0].name, "Via"); | ||
assert_eq!(response.headers[0].value, b"SIP/2.0/UDP example.com;branch=FFFFFFFFFFFFFF;received=10.10.10.10"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the same reason, this also errors, but putting the function pointer in
const
first makes it go away. Interesting.