From a3e285b8d0a756fdf839c517b8e57df55a8e81bd Mon Sep 17 00:00:00 2001 From: youdie006 Date: Fri, 14 Aug 2026 13:49:41 +0900 Subject: [PATCH] Return an error for degenerate Range/ContentRange bounds instead of overflowing Range::bytes and ContentRange::bytes did unchecked u64 arithmetic on the bounds. An exclusive end of 0 (e.g. Range::bytes(0..0)) computed `end - 1`, which panics in debug and, in release, produces a bogus 2^64-byte or negative-width range header; ContentRange had the same class of overflow on Excluded-start (`s + 1`), Excluded-end (`e - 1`), and Unbounded-end with a complete_length of 0 (`max - 1`). Both constructors already return a Result and error on other invalid inputs, so return that same error for degenerate/empty/overflowing bounds via checked arithmetic. Add regression tests. Fixes #231 --- src/common/content_range.rs | 33 +++++++++++++++++++++++++++++---- src/common/range.rs | 17 +++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/common/content_range.rs b/src/common/content_range.rs index 08de5ccf..451645a8 100644 --- a/src/common/content_range.rs +++ b/src/common/content_range.rs @@ -55,21 +55,29 @@ impl ContentRange { ) -> Result { let complete_length = complete_length.into(); + // Use checked arithmetic so degenerate/empty bounds return an error + // instead of overflowing (e.g. an exclusive end of 0). (#231) + let err = || InvalidContentRange { _inner: () }; + let start = match range.start_bound() { Bound::Included(&s) => s, - Bound::Excluded(&s) => s + 1, + Bound::Excluded(&s) => s.checked_add(1).ok_or_else(err)?, Bound::Unbounded => 0, }; let end = match range.end_bound() { Bound::Included(&e) => e, - Bound::Excluded(&e) => e - 1, + Bound::Excluded(&e) => e.checked_sub(1).ok_or_else(err)?, Bound::Unbounded => match complete_length { - Some(max) => max - 1, - None => return Err(InvalidContentRange { _inner: () }), + Some(max) => max.checked_sub(1).ok_or_else(err)?, + None => return Err(err()), }, }; + if start > end { + return Err(err()); + } + Ok(ContentRange { range: Some((start, end)), complete_length, @@ -236,3 +244,20 @@ test_header!(test_bytes_unknown_range, vec![b"bytes 1-2-3/500"], None::); */ + +#[cfg(test)] +mod tests { + use super::ContentRange; + + #[test] + fn bytes_rejects_degenerate_bounds() { + // #231: bounds that would underflow or produce start > end must return + // an error instead of overflowing the u64 arithmetic. + assert!(ContentRange::bytes(0u64..0u64, 500u64).is_err()); + assert!(ContentRange::bytes(3u64..3u64, 500u64).is_err()); + assert!(ContentRange::bytes(.., 0u64).is_err()); + // Valid ranges still work. + assert!(ContentRange::bytes(0u64..500u64, 500u64).is_ok()); + assert!(ContentRange::bytes(0u64..=499u64, 500u64).is_ok()); + } +} diff --git a/src/common/range.rs b/src/common/range.rs index aef73758..0c3fdf11 100644 --- a/src/common/range.rs +++ b/src/common/range.rs @@ -53,6 +53,11 @@ impl Range { let v = match (bounds.start_bound(), bounds.end_bound()) { (Bound::Included(start), Bound::Included(end)) => format!("bytes={}-{}", start, end), (Bound::Included(start), Bound::Excluded(&end)) => { + // An exclusive end of 0 (or <= start) has no last byte, so + // `end - 1` would underflow / produce an invalid range. (#231) + if end <= *start { + return Err(InvalidRange { _inner: () }); + } format!("bytes={}-{}", start, end - 1) } (Bound::Included(start), Bound::Unbounded) => format!("bytes={}-", start), @@ -456,3 +461,15 @@ fn test_to_unsatisfiable_range_suffix() { let bounds = range.satisfiable_ranges(100).next(); assert_eq!(bounds, None); } + +#[test] +fn test_bytes_rejects_degenerate_bounds() { + // #231: an exclusive end that would underflow (`0`) or produce an invalid + // start > last range must error rather than panic or emit a bogus range. + assert!(Range::bytes(0u64..0u64).is_err()); + assert!(Range::bytes(3u64..3u64).is_err()); + assert!(Range::bytes(5u64..3u64).is_err()); + // Valid ranges still work. + assert!(Range::bytes(0u64..500u64).is_ok()); + assert!(Range::bytes(0u64..=500u64).is_ok()); +}