chie_core/
partial_chunk.rs

1//! Partial chunk support for range requests.
2//!
3//! This module provides functionality for serving partial content requests,
4//! enabling efficient byte-range retrieval for streaming scenarios.
5//!
6//! # Features
7//!
8//! - HTTP-style range request support (bytes=0-1023)
9//! - Multi-range requests
10//! - Chunk-aligned range calculations
11//! - Efficient partial reads without loading full chunks
12//! - Content-Length and Content-Range header generation
13//!
14//! # Example
15//!
16//! ```
17//! use chie_core::partial_chunk::{RangeRequest, RangeHandler};
18//!
19//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
20//! // Parse a range request
21//! let range = RangeRequest::parse("bytes=0-1023")?;
22//!
23//! // Create a range handler for content
24//! let handler = RangeHandler::new(1_000_000, 262_144); // 1MB total, 256KB chunks
25//!
26//! // Get the chunks needed for this range
27//! let chunks = handler.get_required_chunks(&range)?;
28//!
29//! println!("Need chunks: {:?}", chunks);
30//! # Ok(())
31//! # }
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::fmt;
36use thiserror::Error;
37
38/// Default chunk size for range calculations (256 KB)
39const DEFAULT_CHUNK_SIZE: u64 = 256 * 1024;
40
41/// Errors that can occur during range request processing
42#[derive(Debug, Error)]
43pub enum RangeError {
44    #[error("Invalid range syntax: {0}")]
45    InvalidSyntax(String),
46
47    #[error("Range not satisfiable: {0}")]
48    NotSatisfiable(String),
49
50    #[error("Invalid range bounds: start={0}, end={1}")]
51    InvalidBounds(u64, u64),
52
53    #[error("Range exceeds content length: {0} > {1}")]
54    ExceedsContent(u64, u64),
55}
56
57/// Represents a byte range request
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ByteRange {
60    /// Start byte (inclusive)
61    pub start: u64,
62    /// End byte (inclusive, None means to end of content)
63    pub end: Option<u64>,
64}
65
66impl ByteRange {
67    /// Create a new byte range
68    #[must_use]
69    pub const fn new(start: u64, end: Option<u64>) -> Self {
70        Self { start, end }
71    }
72
73    /// Create a range from start to end (inclusive)
74    #[must_use]
75    pub const fn from_to(start: u64, end: u64) -> Self {
76        Self {
77            start,
78            end: Some(end),
79        }
80    }
81
82    /// Create a range from start to end of content
83    #[must_use]
84    pub const fn from_start(start: u64) -> Self {
85        Self { start, end: None }
86    }
87
88    /// Create a range for the last N bytes
89    #[must_use]
90    pub const fn suffix(count: u64) -> Self {
91        Self {
92            start: 0,
93            end: Some(count),
94        }
95    }
96
97    /// Normalize the range to absolute positions given content length
98    pub fn normalize(&self, content_length: u64) -> Result<(u64, u64), RangeError> {
99        let start = self.start;
100        let end = self.end.unwrap_or(content_length.saturating_sub(1));
101
102        // Validate bounds
103        if start > end {
104            return Err(RangeError::InvalidBounds(start, end));
105        }
106
107        if end >= content_length {
108            return Err(RangeError::ExceedsContent(end, content_length));
109        }
110
111        Ok((start, end))
112    }
113
114    /// Calculate the length of this range (inclusive)
115    #[must_use]
116    pub const fn length(&self) -> u64 {
117        match self.end {
118            Some(end) => end.saturating_sub(self.start) + 1,
119            None => u64::MAX, // Unknown until normalized
120        }
121    }
122}
123
124impl fmt::Display for ByteRange {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "bytes {}-", self.start)?;
127        if let Some(end) = self.end {
128            write!(f, "{end}")
129        } else {
130            write!(f, "*")
131        }
132    }
133}
134
135/// A range request (may contain multiple ranges)
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct RangeRequest {
138    /// List of requested byte ranges
139    pub ranges: Vec<ByteRange>,
140}
141
142impl RangeRequest {
143    /// Create a new range request with a single range
144    #[must_use]
145    pub fn new(range: ByteRange) -> Self {
146        Self {
147            ranges: vec![range],
148        }
149    }
150
151    /// Create a range request with multiple ranges
152    #[must_use]
153    pub fn multi(ranges: Vec<ByteRange>) -> Self {
154        Self { ranges }
155    }
156
157    /// Parse an HTTP Range header value (e.g., "bytes=0-1023")
158    pub fn parse(header: &str) -> Result<Self, RangeError> {
159        let header = header.trim();
160
161        // Check for "bytes=" prefix
162        if !header.starts_with("bytes=") {
163            return Err(RangeError::InvalidSyntax(
164                "Range must start with 'bytes='".to_string(),
165            ));
166        }
167
168        let range_str = &header[6..]; // Skip "bytes="
169        let mut ranges = Vec::new();
170
171        // Parse comma-separated ranges
172        for part in range_str.split(',') {
173            let part = part.trim();
174
175            if part.is_empty() {
176                continue;
177            }
178
179            // Parse individual range (e.g., "0-1023" or "1024-" or "-500")
180            if let Some((start_str, end_str)) = part.split_once('-') {
181                let range = if start_str.is_empty() {
182                    // Suffix range: "-500" means last 500 bytes
183                    let count: u64 = end_str
184                        .parse()
185                        .map_err(|_| RangeError::InvalidSyntax(part.to_string()))?;
186                    ByteRange::suffix(count)
187                } else if end_str.is_empty() {
188                    // Open-ended range: "1024-" means from 1024 to end
189                    let start: u64 = start_str
190                        .parse()
191                        .map_err(|_| RangeError::InvalidSyntax(part.to_string()))?;
192                    ByteRange::from_start(start)
193                } else {
194                    // Full range: "0-1023"
195                    let start: u64 = start_str
196                        .parse()
197                        .map_err(|_| RangeError::InvalidSyntax(part.to_string()))?;
198                    let end: u64 = end_str
199                        .parse()
200                        .map_err(|_| RangeError::InvalidSyntax(part.to_string()))?;
201                    ByteRange::from_to(start, end)
202                };
203
204                ranges.push(range);
205            } else {
206                return Err(RangeError::InvalidSyntax(part.to_string()));
207            }
208        }
209
210        if ranges.is_empty() {
211            return Err(RangeError::InvalidSyntax(
212                "No valid ranges found".to_string(),
213            ));
214        }
215
216        Ok(Self { ranges })
217    }
218
219    /// Check if this is a multi-range request
220    #[must_use]
221    #[inline]
222    pub fn is_multi_range(&self) -> bool {
223        self.ranges.len() > 1
224    }
225
226    /// Get the total number of bytes requested across all ranges
227    pub fn total_bytes(&self, content_length: u64) -> Result<u64, RangeError> {
228        let mut total = 0u64;
229        for range in &self.ranges {
230            let (start, end) = range.normalize(content_length)?;
231            total = total.saturating_add(end.saturating_sub(start) + 1);
232        }
233        Ok(total)
234    }
235}
236
237/// Information about a chunk that needs to be read for a range
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct ChunkRange {
240    /// Chunk index
241    pub chunk_index: u64,
242    /// Byte offset within the chunk to start reading
243    pub offset_in_chunk: u64,
244    /// Number of bytes to read from this chunk
245    pub length: u64,
246}
247
248/// Handles range requests for chunked content
249pub struct RangeHandler {
250    /// Total content length in bytes
251    content_length: u64,
252    /// Chunk size in bytes
253    chunk_size: u64,
254}
255
256impl RangeHandler {
257    /// Create a new range handler
258    #[must_use]
259    pub const fn new(content_length: u64, chunk_size: u64) -> Self {
260        Self {
261            content_length,
262            chunk_size,
263        }
264    }
265
266    /// Create a range handler with default chunk size (256 KB)
267    #[must_use]
268    pub const fn with_default_chunk_size(content_length: u64) -> Self {
269        Self::new(content_length, DEFAULT_CHUNK_SIZE)
270    }
271
272    /// Get the chunks required to satisfy a range request
273    pub fn get_required_chunks(
274        &self,
275        request: &RangeRequest,
276    ) -> Result<Vec<ChunkRange>, RangeError> {
277        let mut chunk_ranges = Vec::new();
278
279        for range in &request.ranges {
280            let (start, end) = range.normalize(self.content_length)?;
281
282            // Calculate which chunks we need
283            let start_chunk = start / self.chunk_size;
284            let end_chunk = end / self.chunk_size;
285
286            for chunk_idx in start_chunk..=end_chunk {
287                let chunk_start = chunk_idx * self.chunk_size;
288                let chunk_end = ((chunk_idx + 1) * self.chunk_size).min(self.content_length) - 1;
289
290                // Calculate the intersection of requested range and this chunk
291                let read_start = start.max(chunk_start);
292                let read_end = end.min(chunk_end);
293
294                let offset_in_chunk = read_start - chunk_start;
295                let length = read_end - read_start + 1;
296
297                chunk_ranges.push(ChunkRange {
298                    chunk_index: chunk_idx,
299                    offset_in_chunk,
300                    length,
301                });
302            }
303        }
304
305        Ok(chunk_ranges)
306    }
307
308    /// Generate Content-Range header value
309    #[must_use]
310    pub fn content_range_header(&self, start: u64, end: u64) -> String {
311        format!("bytes {start}-{end}/{}", self.content_length)
312    }
313
314    /// Check if a range request is satisfiable
315    #[must_use]
316    #[inline]
317    pub fn is_satisfiable(&self, request: &RangeRequest) -> bool {
318        request
319            .ranges
320            .iter()
321            .all(|r| r.normalize(self.content_length).is_ok())
322    }
323
324    /// Get content length
325    #[must_use]
326    #[inline]
327    pub const fn content_length(&self) -> u64 {
328        self.content_length
329    }
330
331    /// Get chunk size
332    #[must_use]
333    #[inline]
334    pub const fn chunk_size(&self) -> u64 {
335        self.chunk_size
336    }
337}
338
339/// Response for a partial content request
340#[derive(Debug, Clone)]
341pub struct PartialResponse {
342    /// HTTP status code (206 for partial content, 416 for not satisfiable)
343    pub status_code: u16,
344    /// Content-Range header value
345    pub content_range: Option<String>,
346    /// Content-Length header value
347    pub content_length: u64,
348    /// Actual data (assembled from chunks)
349    pub data: Vec<u8>,
350}
351
352impl PartialResponse {
353    /// Create a successful partial response (206)
354    #[must_use]
355    pub fn partial_content(content_range: String, data: Vec<u8>) -> Self {
356        let content_length = data.len() as u64;
357        Self {
358            status_code: 206,
359            content_range: Some(content_range),
360            content_length,
361            data,
362        }
363    }
364
365    /// Create a range not satisfiable response (416)
366    #[must_use]
367    pub fn not_satisfiable(total_length: u64) -> Self {
368        Self {
369            status_code: 416,
370            content_range: Some(format!("bytes */{total_length}")),
371            content_length: 0,
372            data: Vec::new(),
373        }
374    }
375
376    /// Create a full content response (200)
377    #[must_use]
378    pub fn full_content(data: Vec<u8>) -> Self {
379        let content_length = data.len() as u64;
380        Self {
381            status_code: 200,
382            content_range: None,
383            content_length,
384            data,
385        }
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_byte_range_new() {
395        let range = ByteRange::new(0, Some(1023));
396        assert_eq!(range.start, 0);
397        assert_eq!(range.end, Some(1023));
398    }
399
400    #[test]
401    fn test_byte_range_normalize() {
402        let range = ByteRange::from_to(0, 1023);
403        let (start, end) = range.normalize(10_000).unwrap();
404        assert_eq!(start, 0);
405        assert_eq!(end, 1023);
406
407        // Test range exceeding content length
408        let range = ByteRange::from_to(0, 20_000);
409        assert!(range.normalize(10_000).is_err());
410    }
411
412    #[test]
413    fn test_byte_range_length() {
414        let range = ByteRange::from_to(0, 1023);
415        assert_eq!(range.length(), 1024);
416
417        let range = ByteRange::from_start(1000);
418        assert_eq!(range.length(), u64::MAX); // Unknown until normalized
419    }
420
421    #[test]
422    fn test_range_request_parse_simple() {
423        let request = RangeRequest::parse("bytes=0-1023").unwrap();
424        assert_eq!(request.ranges.len(), 1);
425        assert_eq!(request.ranges[0].start, 0);
426        assert_eq!(request.ranges[0].end, Some(1023));
427    }
428
429    #[test]
430    fn test_range_request_parse_open_ended() {
431        let request = RangeRequest::parse("bytes=1024-").unwrap();
432        assert_eq!(request.ranges.len(), 1);
433        assert_eq!(request.ranges[0].start, 1024);
434        assert_eq!(request.ranges[0].end, None);
435    }
436
437    #[test]
438    fn test_range_request_parse_suffix() {
439        let request = RangeRequest::parse("bytes=-500").unwrap();
440        assert_eq!(request.ranges.len(), 1);
441        assert_eq!(request.ranges[0].start, 0);
442        assert_eq!(request.ranges[0].end, Some(500));
443    }
444
445    #[test]
446    fn test_range_request_parse_multi() {
447        let request = RangeRequest::parse("bytes=0-1023,2048-3071").unwrap();
448        assert_eq!(request.ranges.len(), 2);
449        assert_eq!(request.ranges[0].start, 0);
450        assert_eq!(request.ranges[0].end, Some(1023));
451        assert_eq!(request.ranges[1].start, 2048);
452        assert_eq!(request.ranges[1].end, Some(3071));
453    }
454
455    #[test]
456    fn test_range_request_parse_invalid() {
457        assert!(RangeRequest::parse("invalid").is_err());
458        assert!(RangeRequest::parse("bytes=").is_err());
459        assert!(RangeRequest::parse("bytes=abc-def").is_err());
460    }
461
462    #[test]
463    fn test_range_handler_simple_range() {
464        let handler = RangeHandler::new(1_000_000, 256_000);
465        let request = RangeRequest::parse("bytes=0-255999").unwrap();
466        let chunks = handler.get_required_chunks(&request).unwrap();
467
468        assert_eq!(chunks.len(), 1);
469        assert_eq!(chunks[0].chunk_index, 0);
470        assert_eq!(chunks[0].offset_in_chunk, 0);
471        assert_eq!(chunks[0].length, 256_000);
472    }
473
474    #[test]
475    fn test_range_handler_multi_chunk() {
476        let handler = RangeHandler::new(1_000_000, 256_000);
477        let request = RangeRequest::parse("bytes=200000-600000").unwrap();
478        let chunks = handler.get_required_chunks(&request).unwrap();
479
480        // Should span chunks 0, 1, 2
481        assert_eq!(chunks.len(), 3);
482        assert_eq!(chunks[0].chunk_index, 0);
483        assert_eq!(chunks[1].chunk_index, 1);
484        assert_eq!(chunks[2].chunk_index, 2);
485    }
486
487    #[test]
488    fn test_range_handler_content_range_header() {
489        let handler = RangeHandler::new(1_000_000, 256_000);
490        let header = handler.content_range_header(0, 1023);
491        assert_eq!(header, "bytes 0-1023/1000000");
492    }
493
494    #[test]
495    fn test_range_handler_is_satisfiable() {
496        let handler = RangeHandler::new(1_000_000, 256_000);
497
498        let good_request = RangeRequest::parse("bytes=0-1023").unwrap();
499        assert!(handler.is_satisfiable(&good_request));
500
501        let bad_request = RangeRequest::parse("bytes=0-2000000").unwrap();
502        assert!(!handler.is_satisfiable(&bad_request));
503    }
504
505    #[test]
506    fn test_partial_response_partial_content() {
507        let data = vec![1u8, 2, 3, 4];
508        let response = PartialResponse::partial_content("bytes 0-3/100".to_string(), data);
509        assert_eq!(response.status_code, 206);
510        assert_eq!(response.content_length, 4);
511        assert_eq!(response.content_range.unwrap(), "bytes 0-3/100");
512    }
513
514    #[test]
515    fn test_partial_response_not_satisfiable() {
516        let response = PartialResponse::not_satisfiable(100);
517        assert_eq!(response.status_code, 416);
518        assert_eq!(response.content_range.unwrap(), "bytes */100");
519    }
520
521    #[test]
522    fn test_partial_response_full_content() {
523        let data = vec![1u8; 100];
524        let response = PartialResponse::full_content(data);
525        assert_eq!(response.status_code, 200);
526        assert_eq!(response.content_length, 100);
527        assert!(response.content_range.is_none());
528    }
529
530    #[test]
531    fn test_range_request_total_bytes() {
532        let request = RangeRequest::parse("bytes=0-1023,2048-3071").unwrap();
533        let total = request.total_bytes(10_000).unwrap();
534        assert_eq!(total, 2048); // 1024 + 1024
535    }
536}