Skip to content
Merged
Show file tree
Hide file tree
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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,14 @@ client

`206 Partial Content` and `416 Range Not Satisfiable` are visible through
`Response::is_partial_content()` and `Response::is_range_not_satisfiable()`.
`Response::content_range()` parses `Content-Range` into `ContentRange`, using
`start` and `end` for satisfiable ranges such as `bytes 10-19/200`, and no
`start` or `end` for unsatisfied ranges such as `bytes */200`.
`Response::content_range()` parses a bounded singleton `Content-Range` into
`ContentRange`, using `start` and `end` for satisfiable byte ranges such as
`bytes 10-19/200`, and no `start` or `end` for unsatisfied ranges such as
`bytes */200`. The typed parser accepts HTTP optional whitespace around the
field value, emits canonical `bytes start-end/length` or `bytes */length`
formatting, and rejects duplicate fields, unsupported units, malformed
separators, control bytes, overflow, inverted ranges, and ranges whose last
byte is not below a known complete length.

`If-Range` is available through bounded request helpers that compose with the
range helpers: `if_range_etag(etag)` writes a single strong entity-tag
Expand Down Expand Up @@ -1890,7 +1895,7 @@ gain additional HTTP/2 header-block handling.
| Accept-Encoding | Client `accept_encoding`, `accept_encoding_with_q`, and gzip/deflate/br/identity helpers format bounded `Accept-Encoding` request metadata through the shared `rttp-protocol` type; server `Request::accept_encoding()` and `HttpRequest::accept_encoding()` parse received fields into `HttpRequestAcceptEncodings` | No compression, decompression, content negotiation, retries, or transport changes |
| Upgrade and tunnel handoff | `CONNECT` returns the tunnel socket after a successful `200`; `upgrade()` returns the socket after `101 Switching Protocols` and skips interim `1xx` responses | Upgraded protocols are handed to the caller and are not parsed by `rttp_client` |
| Redirects | Auto-redirect covers 301, 302, 303, 307, and 308 method/body behavior, relative and absolute `Location` resolution, same- and cross-authority header handling, loop detection, and redirect bounds | Redirects are HTTP client behavior, not a browser policy implementation |
| Byte ranges | `range`, `range_from`, `range_suffix`, `if_range_etag`, and `if_range_date` emit bounded HTTP/1.1 range request metadata; `Response::content_range`, `accept_ranges`, `is_partial_content`, and `is_range_not_satisfiable` expose `Content-Range`, `Accept-Ranges`, `206`, and `416` metadata while preserving raw headers | No Range request generation from `Accept-Ranges`, client-side `If-Range` evaluation, partial response engine, byte serving, content slicing, download resume, automatic retry/replay, cache storage, redirect handling, status-policy behavior, multipart range generation, or automatic cache validation policy |
| Byte ranges | `range`, `range_from`, `range_suffix`, `if_range_etag`, and `if_range_date` emit bounded HTTP/1.1 range request metadata; `Response::content_range`, `accept_ranges`, `is_partial_content`, and `is_range_not_satisfiable` expose bounded typed `Content-Range`, `Accept-Ranges`, `206`, and `416` metadata while preserving raw headers | No Range request generation from `Accept-Ranges`, client-side `If-Range` evaluation, partial response engine, byte serving, content slicing, download resume, automatic retry/replay, cache storage, redirect handling, status-policy behavior, multipart range generation, or automatic cache validation policy |
| Accept-Patch | `Response::accept_patch` parses repeated bounded `Accept-Patch` response fields through the shared `AcceptPatch` type into ordered `MediaType` values while preserving raw headers on parse errors | No PATCH routing, payload decoding, media-type negotiation, method selection, retry, or automatic follow-up request |
| Accept-Post | `Response::accept_post` parses repeated bounded `Accept-Post` response fields through the shared `AcceptPost` type into ordered `MediaType` values while preserving raw headers on parse errors | No POST routing, payload decoding, media-type negotiation, method selection, retry, or automatic follow-up request |
| Conditional requests | `if_none_match`, `if_match`, `if_modified_since`, and `if_unmodified_since` emit bounded HTTP/1.1 validators; the date helpers validate and emit through the shared protocol `IfModifiedSince` and `IfUnmodifiedSince` types; `Response::is_not_modified`, `is_precondition_failed`, typed bounded `etag`, `delta_base`, and `last_modified` expose `304`/`412` and delta-base metadata while preserving raw headers | One ETag validator per helper call, `If-Range` is range-scoped, no cache storage, no cached-entity lookup, no automatic revalidation, no delta application, and no cache-control engine |
Expand Down
22 changes: 19 additions & 3 deletions crates/rttp-protocol/src/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,8 @@ impl ContentRange {
));
}

validate_value(value, MAX_CONTENT_RANGE_VALUE_BYTES, "Content-Range")
.map_err(ContentRangeParseError::new)?;
let value = value.trim();
validate_content_range_value(value).map_err(ContentRangeParseError::new)?;
let value = trim_http_ows(value);
let Some((unit, range)) = value.split_once(' ') else {
return Err(ContentRangeParseError::new(
"invalid Content-Range header value",
Expand Down Expand Up @@ -259,6 +258,23 @@ fn validate_value(value: &str, maximum_length: usize, name: &str) -> Result<(),
Ok(())
}

fn validate_content_range_value(value: &str) -> Result<(), String> {
if value.len() > MAX_CONTENT_RANGE_VALUE_BYTES {
return Err("Content-Range header value is too large".to_string());
}
if value
.bytes()
.any(|byte| byte.is_ascii_control() && byte != b'\t')
{
return Err("invalid Content-Range header value".to_string());
}
Ok(())
}

fn trim_http_ows(value: &str) -> &str {
value.trim_matches([' ', '\t'])
}

fn parse_range_member(value: &str) -> Result<ByteRangeSpec, RangeParseError> {
if value.is_empty() {
return Err(RangeParseError::new("invalid Range member"));
Expand Down
30 changes: 30 additions & 0 deletions crates/rttp-protocol/tests/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ fn content_range_parses_satisfied_unknown_and_unsatisfied_forms() {
assert_eq!("bytes 0-499/1234", satisfied.header_value());
}

#[test]
fn content_range_trims_http_ows_and_formats_canonical_values() {
let content_range =
ContentRange::parse("\tBYTES 0003-0006/0010 ").expect("OWS-padded content range");
let unsatisfied = ContentRange::parse(" bytes */0010\t").expect("OWS-padded unsatisfied range");

assert_eq!(
ContentRange::Bytes {
start: 3,
end: 6,
complete_length: Some(10),
},
content_range
);
assert_eq!("bytes 3-6/10", content_range.header_value());
assert_eq!(
ContentRange::Unsatisfied {
complete_length: 10,
},
unsatisfied
);
assert_eq!("bytes */10", unsatisfied.header_value());
}

#[test]
fn content_range_rejects_repeated_field_values() {
assert!(ContentRange::parse_values(["bytes 0-1/4", "bytes 2-3/4"]).is_err());
Expand All @@ -82,6 +106,12 @@ fn range_and_content_range_reject_invalid_syntax_controls_and_overflow() {
"bytes 0-2/2",
"bytes */*",
"bytes */18446744073709551616",
"bytes\t0-1/2",
"bytes 0-1 /\t2",
"bytes 0-1/2, bytes 2-3/4",
"bytes 0-1/2/",
"bytes 0-1",
"bytes 0-1/",
"bytes 0-1/2\n",
] {
assert!(
Expand Down
7 changes: 6 additions & 1 deletion crates/rttp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,12 @@ These helpers only declare and inspect metadata. RTTP does not generate
`Range` requests, create a partial response engine, serve files, resume
downloads, or choose redirect, retry, or status-policy behavior. Resolved
range sets are serialized by `HttpResponse::partial_content` and
`HttpResponse::partial_content_ranges`.
`HttpResponse::partial_content_ranges`. `HttpResponse::content_range()` parses
attached singleton `Content-Range` fields into `HttpContentRange`, accepting
satisfied byte ranges and `bytes */length` unsatisfied ranges, canonicalizing
typed formatting, and rejecting duplicates, unsupported units, malformed
separators, control bytes, overflow, inverted ranges, and ranges beyond a known
complete length while leaving raw fields available through `HttpResponse::header`.

## Accept-Patch response metadata

Expand Down