Skip to content
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

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 125 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Author

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.

}
}

/// Sets first-line parsing to SIP
pub const fn set_sip_protocol_parser(mut self) -> Self {
self.version_parser = SIP_VERSION_PARSER;
self
Copy link
Author

Choose a reason for hiding this comment

The 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:

error[E0658]: mutable references are not allowed in constant functions
   --> src/lib.rs:287:31
    |
287 |           self.version_parser = match protocol {
    |  _______________________________^
288 | |             Protocol::Http => HTTP_VERSION_PARSER,
289 | |             Protocol::Sip => SIP_VERSION_PARSER,
290 | |         };
    | |_________^
    |
    = note: see issue #57349 <https://github.com/rust-lang/rust/issues/57349> for more information

which is weird :|

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gootorov Similar code works fine here. What is the difference?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, is it only the attribute type, or the Protocol enum too?

}

/// 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
}
Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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");
}
}