Skip to content

Commit c9e5caa

Browse files
committed
improve
1 parent a5fdbd1 commit c9e5caa

4 files changed

Lines changed: 104 additions & 92 deletions

File tree

objectstore-server/src/endpoints/objects.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async fn object_get(
7272
let header_str = value
7373
.to_str()
7474
.map_err(|_| ApiError::Client("invalid Range header".into()))?;
75-
match ByteRange::parse(header_str) {
75+
match ByteRange::try_from(header_str) {
7676
Ok(range) => Some(range),
7777
Err(RangeError::UnknownUnit | RangeError::MultiRangeNotSupported) => None,
7878
Err(e) => return Err(ApiError::Client(format!("invalid Range header: {e}"))),
@@ -115,10 +115,7 @@ async fn object_get(
115115
http::header::CONTENT_LENGTH,
116116
content_range.len().to_string().parse().unwrap(),
117117
);
118-
resp_headers.insert(
119-
http::header::CONTENT_RANGE,
120-
content_range.to_header_value().parse().unwrap(),
121-
);
118+
resp_headers.insert(http::header::CONTENT_RANGE, content_range.to_header_value());
122119
}
123120

124121
Ok(response)

objectstore-service/src/backend/gcs.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,13 +666,11 @@ impl Backend for GcsBackend {
666666
let mut download_url = object_url;
667667
download_url.query_pairs_mut().append_pair("alt", "media");
668668

669-
let range_header = range.map(|r| r.to_header_value());
670-
671669
let payload_response = self
672670
.with_retry("get_payload", || async {
673671
let mut req = self.request(Method::GET, download_url.clone()).await?;
674-
if let Some(ref value) = range_header {
675-
req = req.header(header::RANGE, value);
672+
if let Some(r) = range {
673+
req = req.header(header::RANGE, r.to_header_value());
676674
}
677675
let resp = req
678676
.send()

objectstore-service/src/backend/s3_compatible.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ impl<T: TokenProvider> Backend for S3CompatibleBackend<T> {
310310

311311
let extra_headers = range.map(|r| {
312312
let mut headers = reqwest::header::HeaderMap::new();
313-
headers.insert(reqwest::header::RANGE, r.to_header_value().parse().unwrap());
313+
headers.insert(reqwest::header::RANGE, r.to_header_value());
314314
headers
315315
});
316316

objectstore-types/src/range.rs

Lines changed: 99 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,45 @@
1-
//! HTTP Range request types for partial content retrieval.
2-
//!
3-
//! [`ByteRange`] represents a parsed single byte-range from a `Range` HTTP
4-
//! header. [`ContentRange`] describes which portion of an object is present in
5-
//! a response body, used to build `Content-Range` response headers and choose
6-
//! between 200 and 206 status codes.
1+
//! Types for HTTP range requests.
72
83
use std::fmt;
94

10-
/// A single byte-range parsed from the `Range` HTTP request header.
11-
///
12-
/// Only the `bytes` range unit is supported, and only a single range specifier
13-
/// (multi-range requests are rejected at parse time).
5+
use http::header::HeaderValue;
6+
7+
/// A byte range.
148
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159
pub enum ByteRange {
16-
/// `bytes=N-M` — from byte offset N to byte offset M (both inclusive).
17-
FromTo(u64, u64),
18-
/// `bytes=N-` — from byte offset N to the end of the representation.
10+
/// Bounded range with start and end, inclusive
11+
Inclusive(u64, u64),
12+
/// From offset X onwards
1913
From(u64),
20-
/// `bytes=-N` — the last N bytes of the representation.
21-
Suffix(u64),
14+
/// Last X bytes
15+
Last(u64),
2216
}
2317

2418
impl ByteRange {
25-
/// Parses a `Range` header value into a [`ByteRange`].
19+
/// Formats this range as a `Range` request header value (e.g. `bytes=0-499`).
2620
///
27-
/// Only `bytes=` ranges with a single specifier are accepted. Multi-range
28-
/// requests (containing commas) and non-`bytes` units are rejected.
29-
pub fn parse(header: &str) -> Result<Self, RangeError> {
30-
let lower = header.to_ascii_lowercase();
31-
let spec = lower
32-
.strip_prefix("bytes=")
33-
.ok_or(RangeError::UnknownUnit)?;
34-
35-
if spec.contains(',') {
36-
return Err(RangeError::MultiRangeNotSupported);
37-
}
38-
39-
let (start_str, end_str) = spec.split_once('-').ok_or(RangeError::InvalidRange)?;
40-
41-
if start_str.is_empty() {
42-
// bytes=-N (suffix)
43-
let n: u64 = end_str.parse().map_err(|_| RangeError::InvalidRange)?;
44-
if n == 0 {
45-
return Err(RangeError::InvalidRange);
46-
}
47-
Ok(ByteRange::Suffix(n))
48-
} else if end_str.is_empty() {
49-
// bytes=N- (from offset to end)
50-
let start: u64 = start_str.parse().map_err(|_| RangeError::InvalidRange)?;
51-
Ok(ByteRange::From(start))
52-
} else {
53-
// bytes=N-M
54-
let start: u64 = start_str.parse().map_err(|_| RangeError::InvalidRange)?;
55-
let end: u64 = end_str.parse().map_err(|_| RangeError::InvalidRange)?;
56-
if start > end {
57-
return Err(RangeError::InvalidRange);
58-
}
59-
Ok(ByteRange::FromTo(start, end))
60-
}
61-
}
62-
63-
/// Formats this range as a `Range` header value (e.g. `bytes=0-499`).
64-
pub fn to_header_value(&self) -> String {
65-
match self {
66-
ByteRange::FromTo(s, e) => format!("bytes={s}-{e}"),
21+
/// The returned value is always valid ASCII and can be inserted directly
22+
/// into an HTTP header map.
23+
pub fn to_header_value(&self) -> HeaderValue {
24+
let s = match self {
25+
ByteRange::Inclusive(s, e) => format!("bytes={s}-{e}"),
6726
ByteRange::From(s) => format!("bytes={s}-"),
68-
ByteRange::Suffix(n) => format!("bytes=-{n}"),
69-
}
27+
ByteRange::Last(n) => format!("bytes=-{n}"),
28+
};
29+
// SAFETY: the format only contains ASCII digits, hyphens, and the
30+
// literal prefix "bytes=", which are all valid header value bytes.
31+
HeaderValue::from_str(&s).expect("ByteRange always produces a valid header value")
7032
}
7133

7234
/// Resolves this range against a known total size, returning the concrete
73-
/// byte offsets and total, or `None` if the range is unsatisfiable.
35+
/// byte offsets and total, or [`None`] if the range is unsatisfiable.
7436
pub fn resolve(self, total: u64) -> Option<ContentRange> {
7537
if total == 0 {
7638
return None;
7739
}
7840

7941
let (start, end) = match self {
80-
ByteRange::FromTo(s, e) => {
42+
ByteRange::Inclusive(s, e) => {
8143
if s >= total {
8244
return None;
8345
}
@@ -89,7 +51,7 @@ impl ByteRange {
8951
}
9052
(s, total - 1)
9153
}
92-
ByteRange::Suffix(n) => {
54+
ByteRange::Last(n) => {
9355
let start = total.saturating_sub(n);
9456
(start, total - 1)
9557
}
@@ -99,6 +61,52 @@ impl ByteRange {
9961
}
10062
}
10163

64+
/// Parses a `Range` request header string into a [`ByteRange`].
65+
///
66+
/// Only `bytes=` ranges with a single specifier are accepted. Multi-range
67+
/// requests (containing commas) and non-`bytes` units are rejected.
68+
///
69+
/// Converting a [`http::header::HeaderValue`] to `&str` via
70+
/// [`HeaderValue::to_str`] is the caller's responsibility, since that
71+
/// conversion can fail when the value contains non-visible-ASCII bytes.
72+
impl TryFrom<&str> for ByteRange {
73+
type Error = RangeError;
74+
75+
fn try_from(header: &str) -> Result<Self, Self::Error> {
76+
let lower = header.to_ascii_lowercase();
77+
let spec = lower
78+
.strip_prefix("bytes=")
79+
.ok_or(RangeError::UnknownUnit)?;
80+
81+
if spec.contains(',') {
82+
return Err(RangeError::MultiRangeNotSupported);
83+
}
84+
85+
let (start_str, end_str) = spec.split_once('-').ok_or(RangeError::InvalidRange)?;
86+
87+
if start_str.is_empty() {
88+
// bytes=-N (suffix / last N bytes)
89+
let n: u64 = end_str.parse().map_err(|_| RangeError::InvalidRange)?;
90+
if n == 0 {
91+
return Err(RangeError::InvalidRange);
92+
}
93+
Ok(ByteRange::Last(n))
94+
} else if end_str.is_empty() {
95+
// bytes=N- (from offset to end)
96+
let start: u64 = start_str.parse().map_err(|_| RangeError::InvalidRange)?;
97+
Ok(ByteRange::From(start))
98+
} else {
99+
// bytes=N-M (inclusive range)
100+
let start: u64 = start_str.parse().map_err(|_| RangeError::InvalidRange)?;
101+
let end: u64 = end_str.parse().map_err(|_| RangeError::InvalidRange)?;
102+
if start > end {
103+
return Err(RangeError::InvalidRange);
104+
}
105+
Ok(ByteRange::Inclusive(start, end))
106+
}
107+
}
108+
}
109+
102110
/// Describes which bytes of the full object are present in the response body.
103111
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104112
pub struct ContentRange {
@@ -156,8 +164,14 @@ impl ContentRange {
156164
}
157165

158166
/// Formats the value for a `Content-Range` response header.
159-
pub fn to_header_value(&self) -> String {
160-
format!("bytes {}-{}/{}", self.start, self.end, self.total)
167+
///
168+
/// The returned value is always valid ASCII and can be inserted directly
169+
/// into an HTTP header map.
170+
pub fn to_header_value(&self) -> HeaderValue {
171+
let s = format!("bytes {}-{}/{}", self.start, self.end, self.total);
172+
// SAFETY: the format only contains ASCII digits, spaces, hyphens,
173+
// and slashes, which are all valid header value bytes.
174+
HeaderValue::from_str(&s).expect("ContentRange always produces a valid header value")
161175
}
162176
}
163177

@@ -200,59 +214,59 @@ mod tests {
200214
#[test]
201215
fn parse_from_to() {
202216
assert_eq!(
203-
ByteRange::parse("bytes=0-499"),
204-
Ok(ByteRange::FromTo(0, 499))
217+
ByteRange::try_from("bytes=0-499"),
218+
Ok(ByteRange::Inclusive(0, 499))
205219
);
206220
}
207221

208222
#[test]
209223
fn parse_from() {
210-
assert_eq!(ByteRange::parse("bytes=500-"), Ok(ByteRange::From(500)));
224+
assert_eq!(ByteRange::try_from("bytes=500-"), Ok(ByteRange::From(500)));
211225
}
212226

213227
#[test]
214228
fn parse_suffix() {
215-
assert_eq!(ByteRange::parse("bytes=-100"), Ok(ByteRange::Suffix(100)));
229+
assert_eq!(ByteRange::try_from("bytes=-100"), Ok(ByteRange::Last(100)));
216230
}
217231

218232
#[test]
219233
fn parse_rejects_multi_range() {
220234
assert_eq!(
221-
ByteRange::parse("bytes=0-10, 20-30"),
235+
ByteRange::try_from("bytes=0-10, 20-30"),
222236
Err(RangeError::MultiRangeNotSupported)
223237
);
224238
}
225239

226240
#[test]
227241
fn parse_case_insensitive() {
228242
assert_eq!(
229-
ByteRange::parse("Bytes=0-499"),
230-
Ok(ByteRange::FromTo(0, 499))
243+
ByteRange::try_from("Bytes=0-499"),
244+
Ok(ByteRange::Inclusive(0, 499))
231245
);
232-
assert_eq!(ByteRange::parse("BYTES=100-"), Ok(ByteRange::From(100)));
246+
assert_eq!(ByteRange::try_from("BYTES=100-"), Ok(ByteRange::From(100)));
233247
}
234248

235249
#[test]
236250
fn parse_returns_unknown_unit_for_non_bytes() {
237-
assert_eq!(ByteRange::parse("items=0-10"), Err(RangeError::UnknownUnit));
251+
assert_eq!(ByteRange::try_from("items=0-10"), Err(RangeError::UnknownUnit));
238252
}
239253

240254
#[test]
241255
fn parse_rejects_inverted_range() {
242256
assert_eq!(
243-
ByteRange::parse("bytes=500-100"),
257+
ByteRange::try_from("bytes=500-100"),
244258
Err(RangeError::InvalidRange)
245259
);
246260
}
247261

248262
#[test]
249263
fn parse_rejects_zero_suffix() {
250-
assert_eq!(ByteRange::parse("bytes=-0"), Err(RangeError::InvalidRange));
264+
assert_eq!(ByteRange::try_from("bytes=-0"), Err(RangeError::InvalidRange));
251265
}
252266

253267
#[test]
254268
fn resolve_from_to() {
255-
let range = ByteRange::FromTo(0, 499).resolve(1000);
269+
let range = ByteRange::Inclusive(0, 499).resolve(1000);
256270
assert_eq!(
257271
range,
258272
Some(ContentRange {
@@ -265,7 +279,7 @@ mod tests {
265279

266280
#[test]
267281
fn resolve_from_to_clamped() {
268-
let range = ByteRange::FromTo(0, 9999).resolve(500);
282+
let range = ByteRange::Inclusive(0, 9999).resolve(500);
269283
assert_eq!(
270284
range,
271285
Some(ContentRange {
@@ -291,7 +305,7 @@ mod tests {
291305

292306
#[test]
293307
fn resolve_suffix() {
294-
let range = ByteRange::Suffix(100).resolve(1000);
308+
let range = ByteRange::Last(100).resolve(1000);
295309
assert_eq!(
296310
range,
297311
Some(ContentRange {
@@ -304,7 +318,7 @@ mod tests {
304318

305319
#[test]
306320
fn resolve_suffix_larger_than_total() {
307-
let range = ByteRange::Suffix(2000).resolve(1000);
321+
let range = ByteRange::Last(2000).resolve(1000);
308322
assert_eq!(
309323
range,
310324
Some(ContentRange {
@@ -317,9 +331,9 @@ mod tests {
317331

318332
#[test]
319333
fn resolve_unsatisfiable() {
320-
assert_eq!(ByteRange::FromTo(1000, 2000).resolve(500), None);
334+
assert_eq!(ByteRange::Inclusive(1000, 2000).resolve(500), None);
321335
assert_eq!(ByteRange::From(500).resolve(500), None);
322-
assert_eq!(ByteRange::FromTo(0, 0).resolve(0), None);
336+
assert_eq!(ByteRange::Inclusive(0, 0).resolve(0), None);
323337
}
324338

325339
#[test]
@@ -353,9 +367,12 @@ mod tests {
353367

354368
#[test]
355369
fn byte_range_to_header_value() {
356-
assert_eq!(ByteRange::FromTo(0, 499).to_header_value(), "bytes=0-499");
370+
assert_eq!(
371+
ByteRange::Inclusive(0, 499).to_header_value(),
372+
"bytes=0-499"
373+
);
357374
assert_eq!(ByteRange::From(500).to_header_value(), "bytes=500-");
358-
assert_eq!(ByteRange::Suffix(100).to_header_value(), "bytes=-100");
375+
assert_eq!(ByteRange::Last(100).to_header_value(), "bytes=-100");
359376
}
360377

361378
#[test]

0 commit comments

Comments
 (0)